OSCR

Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.

Code ↔ Paper

11 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 11 matches
  1. [1] § RESULTS › AD, FTLD‐TDP, and FTLD‐tau have lower subcortical and limbic volumes than LBD ↔ subcortical_analyses.ipynb, lines 111–128 · score 0.69 · Lewy body disease, likelihood ratio, ICV normalized postmortem, subcortical volumes, Pairwise, FTLD TDP
  2. [2] § RESULTS › Subcortical and limbic volume loss tracks with cortical thinning differently in each disease ↔ subcortical_cortical_analyses.ipynb, lines 169–176 · score 0.68 · partial Spearman correlations, normalized weighted, ICV normalized, cortical thickness, composite, coefficients
  3. [3] § RESULTS › AD, FTLD‐TDP, and FTLD‐tau have lower subcortical and limbic volumes than LBD ↔ subcortical_cortical_analyses.ipynb, lines 410–432 · score 0.66 · Boxplots compare, Lewy body disease, likelihood ratio, FTLD tau, Pairwise, FTLD TDP
  4. [4] § RESULTS › Polypathological models suggest primary pathology mostly drives subcortical and limbic volume loss ↔ subcortical_analyses.ipynb, lines 2632–2637 · score 0.65 · standardized coefficients, pathology predictors, OLS models, FDR correction, fitting, covariates
  5. [5] § METHODS › Semi‐quantitative neuropathology and histology measures ↔ notebooks/analysis_notebook_10_5_23.ipynb, lines 428–523 · score 0.60 · angular gyrus, cingulate, superior, temporal, field, frontal
  6. [6] § METHODS › Semi‐quantitative neuropathology and histology measures ↔ subcortical_analyses.ipynb, lines 23–54 · score 0.59 · Semi quantitative, antibodies, pons, brainstem, neurons, scores
  7. [7] § METHODS › Statistical analysis › Structural group differences and subcortical–cortical associations ↔ subcortical_cortical_analyses.ipynb, lines 328–407 · score 0.59 · DKT atlas, partial Spearman, cortical region, nucleus, thickness, correlations
  8. [8] § RESULTS › Primary AD is linked to worse subcortical and cortical atrophy than primary LBD in donors with both AD and LBD copathologies ↔ subcortical_analyses.ipynb, lines 1248–1288 · score 0.55 · normalized postmortem subcortical, Pathology burden, Primary pathology, linear, fit, age
  9. [9] § RESULTS › Histologic neuronal loss and gliosis mediate relationships between subcortical and limbic pathology and volume loss ↔ subcortical_analyses.ipynb, lines 545–562 · score 0.53 · regional pathology burden, FDR correction, postmortem subcortical, age, sex, education
  10. [10] § RESULTS › Polypathological models suggest primary pathology mostly drives subcortical and limbic volume loss ↔ subcortical_analyses.ipynb, lines 3070–3218 · score 0.52 · polypathology model, multiple pathologies, FTLD TDP, synuclein, tauopathy, regression
  11. [11] § RESULTS › Primary AD is linked to worse subcortical and cortical atrophy than primary LBD in donors with both AD and LBD copathologies ↔ notebooks/analysis_notebook_10_5_23.ipynb, lines 428–523 · score 0.51 · superior parietal, globus pallidus, cortex, thicknesses, caudate, cortical

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 · 6,168 lines · 195 KB · no license · 6 matches

  1. # %%
  2. """
  3. Imports core Python libraries for data wrangling, statistical analysis, and visualization, including pandas, NumPy, seaborn, and matplotlib.
  4. Also imports tools for t-tests, Tukey post-hoc comparisons, formatted summary tables, custom plot legends.
  5. """
  6. import pandas as pd
  7. import seaborn as sns
  8. import matplotlib.pyplot as plt
  9. import warnings
  10. import numpy as np
  11. from matplotlib.patches import Patch
  12. from scipy.stats import ttest_rel, ttest_ind
  13. from statsmodels.stats.multicomp import pairwise_tukeyhsd
  14. import seaborn as sns
  15. import pandas as pd
  16. import pingouin as pg
  17. from prettytable import PrettyTable
  18. import warnings
  19. import warnings
  20. from statsmodels.tools.sm_exceptions import ConvergenceWarning
  21. # %%
  22. """
  23. Converts semi-quantitative regional pathology scores into numeric values for analysis.
  24. The function identifies region-specific pathology columns, replaces ordinal/string scores such as 'Rare', '1+', '2+', and '3+' with numeric values, converts unavailable entries to NaN, and returns a cleaned copy of the dataframe.
  25. """
  26. def semiq_scores_to_numeric(df):
  27. # All the columns for a given region
  28. # Already done
  29. measures = [ 'Tau', 'ThioPlaques', 'AntibodyPlaques', 'aSyn', 'Ubiquitin',
  30. 'Gliosis', 'NeuronLoss', 'TDP43', 'Other', 'Update', 'Angiopathy']
  31. # Find all the columns with regional pathology scores
  32. regions = [ 'Amyg', 'DG', 'CS', 'EC', 'MF', 'Ang', 'SMT', 'Cing', 'OC',
  33. 'Neocortical', 'CP', 'GP', 'TS', 'Subcortical', 'MB', 'SN',
  34. 'Pons', 'LC', 'Med', 'CB', 'SC', 'Brainstem', 'MC', 'OFC']
  35. # Cross product of these lists
  36. cols_semiq = [x + y for x in regions for y in measures ]
  37. # Conversion rules
  38. repl_semiq = { 'Rare': 0.5, '2+': 2.0, '3+': 3.0, '1+': 1.0, '0': 0.0,
  39. 'Presumed 0': 0.0, 'Not Avail': np.nan, 'Not Done': np.nan,
  40. 'Not Available': np.nan, '.': np.nan }
  41. # Apply the replacements
  42. df_copy = df.copy()
  43. for m in cols_semiq:
  44. if m in df.columns:
  45. df_copy[m] = pd.to_numeric(df[m].replace(repl_semiq), errors='coerce')
  46. return df_copy
  47. # %%
  48. """
  49. Defines a lookup dictionary mapping SynthSeg integer label IDs to their corresponding anatomical structure names.
  50. This mapping is used to translate segmentation label values into readable brain-region names for downstream volume extraction, summaries, and plots.
  51. """
  52. label_to_structure = {
  53. 0: "Background",
  54. 2: "Left cerebral white matter",
  55. 3: "Left cerebral cortex",
  56. 4: "Left lateral ventricle",
  57. 5: "Left inferior lateral ventricle",
  58. 7: "Left cerebellum white matter",
  59. 8: "Left cerebellum cortex",
  60. 10: "Left thalamus",
  61. 11: "Left caudate",
  62. 12: "Left putamen",
  63. 13: "Left pallidum",
  64. 14: "3rd ventricle",
  65. 15: "4th ventricle",
  66. 16: "Brain-stem",
  67. 17: "Left hippocampus",
  68. 18: "Left amygdala",
  69. 24: "CSF (SynthSeg 2.0 only)",
  70. 26: "Left accumbens area",
  71. 28: "Left ventral DC",
  72. 41: "Right cerebral white matter",
  73. 42: "Right cerebral cortex",
  74. 43: "Right lateral ventricle",
  75. 44: "Right inferior lateral ventricle",
  76. 46: "Right cerebellum white matter",
  77. 47: "Right cerebellum cortex",
  78. 49: "Right thalamus",
  79. 50: "Right caudate",
  80. 51: "Right putamen",
  81. 52: "Right pallidum",
  82. 53: "Right hippocampus",
  83. 54: "Right amygdala",
  84. 58: "Right accumbens area",
  85. 60: "Right ventral DC",
  86. }
  87. # %%
  88. """
  89. Loads the main analysis dataframe from a CSV file.
  90. The resulting dataframe, df, is used as the input table for subsequent cleaning, statistical analysis, and visualization steps.
  91. """
  92. df = pd.read_csv("data_frame.csv")
  93. # %%
  94. # %%
  95. """
  96. Computes ICV-normalized postmortem subcortical volumes and performs covariate-adjusted pairwise likelihood-ratio tests across neuropathological diagnostic groups.
  97. The script applies FDR correction to pairwise p-values and generates publication-style box/strip plots with significance annotations for limbic and subcortical structures.
  98. """
  99. import pandas as pd
  100. import numpy as np
  101. import seaborn as sns
  102. import matplotlib.pyplot as plt
  103. from itertools import combinations
  104. from scipy.stats import chi2
  105. from statsmodels.formula.api import ols
  106. import pingouin as pg
  107. df_use = df.copy()
  108. order = ["alzheimer's disease", "lewy body disease", "ftld-tdp", "tauopathies"]
  109. # Pretty x-labels
  110. x_labels = [
  111. "Alzheimer’s\ndisease",
  112. "Lewy body\ndisease",
  113. "FTLD-TDP",
  114. "Tauopathies"
  115. ]
  116. # Panel groups
  117. panelA = ["hippocampus", "amygdala", "accumbens_area"]
  118. panelB = ["thalamus", "caudate", "putamen", "pallidum"]
  119. all_structures = panelA + panelB
  120. covars = ["AgeatDeath", "Sex", "Education", "PMI"]
  121. palette = ["#3366CC", "#DC3912", "#109618", "#FF9900"]
  122. sns.set(style="whitegrid", context="talk", font_scale=1.2)
  123. # ============================================================
  124. # NORMALIZE
  125. # ============================================================
  126. for s in all_structures:
  127. pm = f"postmortem_{s}"
  128. if pm in df_use.columns:
  129. df_use[f"{s}_norm"] = df_use[pm] / df_use["antemortem_icv"]
  130. needed = [f"{s}_norm" for s in all_structures] + ["NPDx1"] + covars
  131. df_use = df_use[needed].dropna()
  132. # ============================================================
  133. # PAIRWISE LRTs
  134. # ============================================================
  135. pairwise_results = []
  136. for s in all_structures:
  137. ycol = f"{s}_norm"
  138. for g1, g2 in combinations(order, 2):
  139. d = df_use[df_use["NPDx1"].isin([g1, g2])]
  140. if len(d) < 10:
  141. continue
  142. reduced = ols(f"{ycol} ~ " + " + ".join(covars), data=d).fit()
  143. full = ols(f"{ycol} ~ C(NPDx1) + " + " + ".join(covars), data=d).fit()
  144. lr = 2*(full.llf - reduced.llf)
  145. df_diff = full.df_model - reduced.df_model
  146. p = chi2.sf(lr, df_diff)
  147. pairwise_results.append({
  148. "Structure": s, "Group1": g1, "Group2": g2, "p_raw": p
  149. })
  150. pairwise_df = pd.DataFrame(pairwise_results)
  151. reject, p_corr = pg.multicomp(pairwise_df["p_raw"], method="fdr_bh")
  152. pairwise_df["p_FDR"] = p_corr
  153. pairwise_df["Sig"] = pairwise_df["p_FDR"].apply(
  154. lambda p: "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
  155. )
  156. # ============================================================
  157. # PLOTTING
  158. # ============================================================
  159. fig = plt.figure(figsize=(26, 18))
  160. # TOP PANEL (3)
  161. gsA = fig.add_gridspec(
  162. 1, 3, left=0.05, right=0.97,
  163. top=0.92, bottom=0.56, wspace=0.33
  164. )
  165. axesA = [fig.add_subplot(gsA[0, k]) for k in range(3)]
  166. # BOTTOM PANEL (4)
  167. gsB = fig.add_gridspec(
  168. 1, 4, left=0.05, right=0.97,
  169. top=0.50, bottom=0.12, wspace=0.30
  170. )
  171. axesB = [fig.add_subplot(gsB[0, k]) for k in range(4)]
  172. axes = axesA + axesB
  173. def plot_struct(ax, s):
  174. ycol = f"{s}_norm"
  175. d = df_use[["NPDx1", ycol]].dropna()
  176. # -------------- PLOT GROUPS --------------
  177. for idx, g in enumerate(order):
  178. vals = d.loc[d["NPDx1"] == g, ycol]
  179. tmp = pd.DataFrame({"group": [g]*len(vals), "y": vals})
  180. sns.boxplot(
  181. data=tmp, x="group", y="y",
  182. color=palette[idx], ax=ax,
  183. width=0.55, fliersize=0,
  184. linewidth=1.3, boxprops=dict(alpha=0.72)
  185. )
  186. sns.stripplot(
  187. data=tmp, x="group", y="y",
  188. color="black", size=4, alpha=0.55,
  189. ax=ax, jitter=0.15
  190. )
  191. # REMOVE DEFAULT LABELS
  192. ax.set_xlabel("")
  193. ax.set_ylabel("")
  194. # Set pretty labels
  195. ax.set_xticklabels(x_labels, fontsize=11)
  196. ymin, ymax = d[ycol].min(), d[ycol].max()
  197. yr = ymax - ymin
  198. ax.set_ylim(ymin - 0.06*yr, ymax + 0.45*yr)
  199. ax.set_title(s.replace("_", " ").capitalize(), fontsize=16, fontweight="bold")
  200. ax.grid(axis="y", linestyle=":", alpha=0.45)
  201. # ---------------- SIG BARS -----------------
  202. pairs = pairwise_df[pairwise_df["Structure"] == s]
  203. y_offset = 0.020 * yr
  204. y_pos = ymax + 0.10 * yr
  205. for _, row in pairs.iterrows():
  206. if row["Sig"]:
  207. g1, g2 = row["Group1"], row["Group2"]
  208. x1 = order.index(g1)
  209. x2 = order.index(g2)
  210. ax.plot([x1, x1, x2, x2],
  211. [y_pos, y_pos+y_offset, y_pos+y_offset, y_pos],
  212. lw=1.35, color="black")
  213. ax.text((x1+x2)/2, y_pos + y_offset*0.8,
  214. row["Sig"], ha="center",
  215. fontsize=13, fontweight="bold")
  216. y_pos += y_offset * 1.9
  217. # Draw all structures
  218. for ax, s in zip(axes, all_structures):
  219. plot_struct(ax, s)
  220. # ============================================================
  221. # GLOBAL LABELS + TITLE
  222. # ============================================================
  223. fig.text(0.001, 0.55, "Normalized volume",
  224. va="center", rotation="vertical",
  225. fontsize=20, fontweight="bold")
  226. fig.text(0.50, 0.06, "Diagnostic groups",
  227. ha="center", fontsize=20, fontweight="bold")
  228. plt.subplots_adjust(top=0.93)
  229. fig.suptitle(
  230. "Postmortem subcortical volumes differentiates neuropathological groups",
  231. fontsize=26, fontweight="bold"
  232. )
  233. plt.tight_layout()
  234. plt.savefig("Postmortem subcortical volumes differentiates neuropathological groups.png", dpi=600, bbox_inches="tight")
  235. plt.show()
  236. # %%
  237. # %%
  238. """
  239. Generates a supplementary DOCX table of pairwise covariate-adjusted likelihood-ratio tests
  240. for ICV-normalized postmortem subcortical volumes across neuropathological diagnostic groups.
  241. The script computes adjusted means, mean differences, standard errors, 95% confidence intervals,
  242. raw p-values, FDR-corrected p-values, and significance labels, then exports publication-ready
  243. tables to a Word document.
  244. """
  245. import pandas as pd
  246. import numpy as np
  247. from itertools import combinations
  248. from statsmodels.formula.api import ols
  249. from scipy.stats import chi2, shapiro
  250. from statsmodels.stats.diagnostic import het_breuschpagan
  251. from docx import Document
  252. from docx.shared import Pt
  253. from docx.enum.table import WD_TABLE_ALIGNMENT
  254. # ============================================================
  255. # SETTINGS
  256. # ============================================================
  257. order = ["alzheimer's disease", "lewy body disease", "ftld-tdp", "tauopathies"]
  258. covars = ["AgeatDeath", "Sex", "Education", "PMI"]
  259. structures = [
  260. "hippocampus","amygdala","accumbens_area",
  261. "thalamus","caudate","putamen","pallidum"
  262. ]
  263. rows = []
  264. # ============================================================
  265. # LOOP OVER STRUCTURES AND GROUP PAIRS
  266. # ============================================================
  267. for s in structures:
  268. ycol = f"{s}_norm"
  269. for g1, g2 in combinations(order, 2):
  270. d = df_use[df_use["NPDx1"].isin([g1, g2])].copy()
  271. if len(d) < 10:
  272. continue
  273. n_g1 = (d["NPDx1"] == g1).sum()
  274. n_g2 = (d["NPDx1"] == g2).sum()
  275. # ----------------- MODELS -----------------
  276. reduced = ols(f"{ycol} ~ " + " + ".join(covars), data=d).fit()
  277. full = ols(f"{ycol} ~ C(NPDx1) + " + " + ".join(covars), data=d).fit()
  278. # ----------------- LRT -----------------
  279. ll_full = full.llf
  280. ll_reduced = reduced.llf
  281. LR = 2 * (ll_full - ll_reduced)
  282. df_diff = full.df_model - reduced.df_model
  283. p_raw = chi2.sf(LR, df_diff)
  284. # ----------------- ADJUSTED MEANS -----------------
  285. d["_pred"] = full.fittedvalues
  286. mean_g1 = d.loc[d["NPDx1"] == g1, "_pred"].mean()
  287. mean_g2 = d.loc[d["NPDx1"] == g2, "_pred"].mean()
  288. diff = mean_g1 - mean_g2
  289. # ----------------- SE + CI -----------------
  290. contrast = np.zeros(len(full.params))
  291. idx1 = list(full.params.index).index(f"C(NPDx1)[T.{g2}]") if f"C(NPDx1)[T.{g2}]" in full.params else None
  292. if idx1 is not None:
  293. contrast[idx1] = -1
  294. se = np.sqrt(contrast @ full.cov_params() @ contrast.T)
  295. ci_low = diff - 1.96 * se
  296. ci_high = diff + 1.96 * se
  297. # ----------------- DIAGNOSTICS -----------------
  298. res = full.resid
  299. shapiro_p = shapiro(res)[1]
  300. lm, lm_pvalue, fval, f_pvalue = het_breuschpagan(res, full.model.exog)
  301. bp_p = lm_pvalue
  302. cooks = full.get_influence().cooks_distance[0].max()
  303. rows.append({
  304. "Structure": s,
  305. "Group1": g1,
  306. "Group2": g2,
  307. "n_g1": n_g1,
  308. "n_g2": n_g2,
  309. "AdjMean_Group1": mean_g1,
  310. "AdjMean_Group2": mean_g2,
  311. "AdjMean_Diff": diff,
  312. "SE_Diff": se,
  313. "CI95_low": ci_low,
  314. "CI95_high": ci_high,
  315. "LR": LR,
  316. "p_raw": p_raw
  317. })
  318. # ============================================================
  319. # DATAFRAME + FDR
  320. # ============================================================
  321. sup_df = pd.DataFrame(rows)
  322. from pingouin import multicomp
  323. _, p_corr = multicomp(sup_df["p_raw"], method="fdr_bh")
  324. sup_df["p_FDR"] = p_corr
  325. sup_df["Sig"] = sup_df["p_FDR"].apply(
  326. lambda p: "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
  327. )
  328. ###############################################################################
  329. # DOCX EXPORT — CLEAN PUBLICATION-READY
  330. ###############################################################################
  331. doc = Document()
  332. # -------------------------------------------------------------
  333. # Global title + caption
  334. # -------------------------------------------------------------
  335. doc.add_heading("Supplementary Table 1", level=1)
  336. caption_text = (
  337. "Pairwise covariate-adjusted likelihood ratio tests across diagnostic groups "
  338. "for each postmortem subcortical structure. Covariate-adjusted marginal means, "
  339. "adjusted mean differences (Δ), standard errors, and 95% confidence intervals "
  340. "are reported. FDR correction was applied globally across all pairwise tests. "
  341. "Scientific notation is used for all numeric values. Significant FDR-corrected "
  342. "p-values are bolded and marked with *, **, or ***."
  343. )
  344. doc.add_paragraph(caption_text)
  345. doc.add_paragraph("\n")
  346. # -------------------------------------------------------------
  347. # Abbreviations
  348. # -------------------------------------------------------------
  349. abbr = {
  350. "alzheimer's disease": "AD",
  351. "lewy body disease": "LBD",
  352. "ftld-tdp": "FTLD-TDP",
  353. "tauopathies": "Tau"
  354. }
  355. # -------------------------------------------------------------
  356. # Helper functions
  357. # -------------------------------------------------------------
  358. def sci(x):
  359. try:
  360. return f"{float(x):.2e}"
  361. except:
  362. return str(x)
  363. def bold_run(cell, text):
  364. p = cell.paragraphs[0]
  365. run = p.add_run(text)
  366. run.bold = True
  367. def add_text(cell, text):
  368. cell.paragraphs[0].add_run(text)
  369. # -------------------------------------------------------------
  370. # Generate tables
  371. # -------------------------------------------------------------
  372. structures_sorted = [
  373. "hippocampus","amygdala","accumbens_area",
  374. "thalamus","caudate","putamen","pallidum"
  375. ]
  376. for s in structures_sorted:
  377. sub = sup_df[sup_df["Structure"] == s].copy()
  378. if len(sub) == 0:
  379. continue
  380. sub = sub.sort_values(by=["p_FDR", "LR"], ascending=[True, False])
  381. doc.add_heading(f"{s.capitalize()}", level=2)
  382. # Build columns
  383. sub["Comparison"] = sub.apply(
  384. lambda r: f"{abbr[r['Group1']]} (n={int(r['n_g1'])}) vs {abbr[r['Group2']]} (n={int(r['n_g2'])})",
  385. axis=1
  386. )
  387. sub["AdjMeans"] = sub.apply(
  388. lambda r: f"{abbr[r['Group1']]} = {sci(r['AdjMean_Group1'])}\n"
  389. f"{abbr[r['Group2']]} = {sci(r['AdjMean_Group2'])}",
  390. axis=1
  391. )
  392. sub["Diff"] = sub.apply(lambda r: sci(r["AdjMean_Diff"]), axis=1)
  393. sub["CI"] = sub.apply(
  394. lambda r: f"[{sci(r['CI95_low'])}, {sci(r['CI95_high'])}]",
  395. axis=1
  396. )
  397. sub["p_FDR_sup"] = sub.apply(lambda r: sci(r["p_FDR"]) + r["Sig"], axis=1)
  398. final_cols = [
  399. "Comparison", "AdjMeans", "Diff",
  400. "SE_Diff", "CI", "LR", "p_raw", "p_FDR_sup"
  401. ]
  402. header_labels = {
  403. "Diff": "Δ (Adj Diff)",
  404. "SE_Diff": "SE",
  405. "p_FDR_sup": "p_FDR"
  406. }
  407. table = doc.add_table(rows=1, cols=len(final_cols))
  408. table.alignment = WD_TABLE_ALIGNMENT.CENTER
  409. table.style = "Table Grid"
  410. hdr = table.rows[0].cells
  411. for j, col in enumerate(final_cols):
  412. bold_run(hdr[j], header_labels.get(col, col))
  413. for _, row in sub.iterrows():
  414. cells = table.add_row().cells
  415. for j, col in enumerate(final_cols):
  416. val = row[col]
  417. if col in ["SE_Diff", "LR", "p_raw"]:
  418. val = sci(val)
  419. if col == "p_FDR_sup" and any(x in str(val) for x in ["*", "**", "***"]):
  420. bold_run(cells[j], str(val))
  421. else:
  422. add_text(cells[j], str(val))
  423. doc.add_paragraph("\n")
  424. # -------------------------------------------------------------
  425. # Save output
  426. # -------------------------------------------------------------
  427. doc.save("group_wise_comparisons.docx")
  428. print("Saved polished Word file: group_wise_comparisons.docx")
  429. # %%
  430. """
  431. Generates a 4-group grid of partial Spearman correlations between regional pathology burden and ICV-normalized postmortem subcortical volumes.
  432. For each diagnostic group and structure, the script selects the relevant pathology marker, adjusts correlations for age at death, sex, PMI, and education, applies FDR correction within each diagnostic group, and saves a publication-style multi-panel figure.
  433. """
  434. matplotlib.rcParams.update({
  435. "font.family": "DejaVu Sans",
  436. "axes.labelsize": 14,
  437. "axes.titlesize": 14,
  438. "xtick.labelsize": 12,
  439. "ytick.labelsize": 12,
  440. })
  441. sns.set(style="whitegrid", context="talk")
  442. warnings.filterwarnings("ignore", category=RuntimeWarning)
  443. np.seterr(divide="ignore", invalid="ignore")
  444. # ============================================================
  445. # INPUT DATA
  446. # ============================================================
  447. df_use = df.copy()
  448. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  449. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  450. # ============================================================
  451. # DISEASE → PRIMARY PATHOLOGY MAPPING (4 GROUPS)
  452. # ============================================================
  453. pathology_markers = {
  454. "alzheimer's disease": "Tau",
  455. "lewy body disease": "aSyn",
  456. "ftld-tdp": "TDP43",
  457. "tauopathies": "Tau",
  458. }
  459. # Labels shown on x-axis
  460. display_labels = {
  461. "Tau": "p-tau",
  462. "aSyn": "α-synuclein",
  463. "TDP43": "TDP-43",
  464. }
  465. # Row labels if you want to use them later
  466. row_titles = {
  467. "alzheimer's disease": "Alzheimer’s disease (AD pathology)",
  468. "lewy body disease": "Lewy body disease (α-synuclein)",
  469. "ftld-tdp": "FTLD-TDP (TDP-43)",
  470. "tauopathies": "FTLD-Tau (p-tau)",
  471. }
  472. # ============================================================
  473. # PREFIX MAP
  474. # ============================================================
  475. region_map = {
  476. "hippocampus": "EC_CS_DG",
  477. "amygdala": "Amyg",
  478. "caudate": "CP",
  479. "putamen": "CP",
  480. "thalamus": "TS",
  481. "pallidum": "GP",
  482. }
  483. structures = ["hippocampus", "amygdala", "caudate", "putamen", "thalamus", "pallidum"]
  484. palette = [
  485. "#56B4E9", "#E69F00", "#009E73",
  486. "#CC79A7", "#F0E442", "#0072B2"
  487. ]
  488. # ============================================================
  489. # NORMALIZE POSTMORTEM VOLUMES
  490. # ============================================================
  491. for s in structures:
  492. pm_col = f"postmortem_{s}"
  493. if pm_col in df_use.columns and "antemortem_icv" in df_use.columns:
  494. df_use[f"{pm_col}_norm"] = df_use[pm_col] / df_use["antemortem_icv"]
  495. # ============================================================
  496. # FIGURE SETUP
  497. # ============================================================
  498. groups = list(pathology_markers.keys()) # 4 groups
  499. nrows, ncols = len(groups), len(structures)
  500. fig, axes = plt.subplots(
  501. nrows, ncols,
  502. figsize=(5.3 * ncols, 4.4 * nrows),
  503. sharey=False
  504. )
  505. # Make axes always 2D
  506. if nrows == 1:
  507. axes = np.expand_dims(axes, axis=0)
  508. plt.subplots_adjust(hspace=0.60, wspace=0.55, top=0.92, bottom=0.07, left=0.09, right=0.97)
  509. # ============================================================
  510. # MAIN LOOP (PARTIAL SPEARMAN)
  511. # ============================================================
  512. for row_idx, (group_name, marker_suffix) in enumerate(pathology_markers.items()):
  513. gdf = df_use[df_use["NPDx1"] == group_name].copy()
  514. # center row title across the full row of subplots
  515. left_ax = axes[row_idx, 0]
  516. right_ax = axes[row_idx, -1]
  517. left_pos = left_ax.get_position()
  518. right_pos = right_ax.get_position()
  519. x_center = (left_pos.x0 + right_pos.x1) / 2
  520. y_top = left_pos.y1 + 0.015
  521. fig.text(
  522. x_center,
  523. y_top,
  524. row_titles[group_name],
  525. ha="center",
  526. va="bottom",
  527. fontsize=22
  528. )
  529. all_stats = []
  530. all_pvals = []
  531. # ---------- compute stats ----------
  532. for col_idx, s in enumerate(structures):
  533. ax = axes[row_idx, col_idx]
  534. post_col = f"postmortem_{s}_norm"
  535. prefix = region_map[s]
  536. if post_col not in gdf.columns:
  537. ax.text(0.5, 0.5, "No volume", ha="center", va="center", fontsize=12)
  538. ax.set_axis_off()
  539. continue
  540. path_candidates = [c for c in gdf.columns if c.lower().startswith(prefix.lower())]
  541. path_cols = [c for c in path_candidates if marker_suffix.lower() in c.lower()]
  542. if not path_cols:
  543. ax.text(0.5, 0.5, "No pathology", ha="center", va="center", fontsize=12)
  544. ax.set_axis_off()
  545. continue
  546. path_col = path_cols[0]
  547. needed_cols = [path_col, post_col, "AgeatDeath", "Sex", "PMI", "Education"]
  548. missing_cols = [c for c in needed_cols if c not in gdf.columns]
  549. if missing_cols:
  550. ax.text(0.5, 0.5, "Missing covariates", ha="center", va="center", fontsize=12)
  551. ax.set_axis_off()
  552. continue
  553. d = gdf[needed_cols].dropna()
  554. if len(d) < 5:
  555. ax.text(0.5, 0.5, "N too small", ha="center", va="center", fontsize=12)
  556. ax.set_axis_off()
  557. continue
  558. res = pg.partial_corr(
  559. data=d,
  560. x=path_col,
  561. y=post_col,
  562. covar=["AgeatDeath", "Sex", "PMI", "Education"],
  563. method="spearman"
  564. )
  565. r = res["r"].iloc[0]
  566. p = res["p-val"].iloc[0]
  567. # Preserve original subplot column index
  568. all_stats.append({
  569. "col_idx": col_idx,
  570. "structure": s,
  571. "n": len(d),
  572. "r": r,
  573. "p_raw": p,
  574. "path_col": path_col,
  575. "post_col": post_col,
  576. })
  577. all_pvals.append(p)
  578. # ---------- FDR correction ----------
  579. if len(all_pvals) > 0:
  580. reject, p_corr = pg.multicomp(all_pvals, method="fdr_bh")
  581. else:
  582. reject, p_corr = [], []
  583. # attach corrected p-values back to stats
  584. for i in range(len(all_stats)):
  585. all_stats[i]["p_fdr"] = p_corr[i]
  586. all_stats[i]["reject"] = reject[i]
  587. # ---------- plot ----------
  588. for stat in all_stats:
  589. col_idx = stat["col_idx"]
  590. s = stat["structure"]
  591. r = stat["r"]
  592. p_raw = stat["p_raw"]
  593. p_fdr = stat["p_fdr"]
  594. path_col = stat["path_col"]
  595. post_col = stat["post_col"]
  596. ax = axes[row_idx, col_idx]
  597. d = gdf[[path_col, post_col, "AgeatDeath", "Sex", "PMI", "Education"]].dropna()
  598. sns.regplot(
  599. data=d,
  600. x=path_col,
  601. y=post_col,
  602. scatter_kws=dict(alpha=0.7, s=45),
  603. line_kws=dict(color=palette[col_idx % len(palette)], lw=2),
  604. color=palette[col_idx % len(palette)],
  605. ax=ax
  606. )
  607. sig = "***" if p_fdr < 0.001 else "**" if p_fdr < 0.01 else "*" if p_fdr < 0.05 else ""
  608. ax.set_title(
  609. f"{s.capitalize()} (ρ={r:.2f}, FDR p={p_fdr:.3f}{sig})",
  610. fontsize=16,
  611. fontweight="bold"
  612. )
  613. ax.set_xlabel(display_labels[marker_suffix], fontsize=16)
  614. ax.set_ylabel("")
  615. ax.grid(True, linestyle=":", alpha=0.5)
  616. ax.margins(x=0.12)
  617. ax.tick_params(axis="y", pad=10)
  618. # ============================================================
  619. # GLOBAL Y-AXIS LABEL
  620. # ============================================================
  621. fig.text(
  622. 0.055, 0.5,
  623. "Normalized volume",
  624. va="center", ha="center",
  625. rotation=90,
  626. fontsize=18, fontweight="bold"
  627. )
  628. # ============================================================
  629. # GLOBAL TITLE
  630. # ============================================================
  631. fig.suptitle(
  632. "Partial Spearman correlation between regional pathology burden and normalized postmortem volume",
  633. fontsize=22,
  634. y=0.99
  635. )
  636. plt.savefig(
  637. "Partial_Spearman_pathology_vs_volume_4groups_corrected.png",
  638. dpi=600,
  639. bbox_inches="tight"
  640. )
  641. plt.show()
  642. # %%
  643. """
  644. Generates an LBD-only 3×3 grid of partial Spearman correlations between thalamic α-synuclein burden and ICV-normalized postmortem limbic/subcortical volumes.
  645. The script adjusts correlations for age at death, sex, PMI, and education, applies FDR correction across tested structures, and saves a publication-style multi-panel regression figure.
  646. """
  647. # ============================================================
  648. # GLOBAL AESTHETICS
  649. # ============================================================
  650. matplotlib.rcParams.update({
  651. "font.family": "DejaVu Sans",
  652. "axes.labelsize": 14,
  653. "axes.titlesize": 14,
  654. "xtick.labelsize": 12,
  655. "ytick.labelsize": 12,
  656. })
  657. sns.set(style="whitegrid", context="talk")
  658. warnings.filterwarnings("ignore", category=RuntimeWarning)
  659. np.seterr(divide="ignore", invalid="ignore")
  660. # ============================================================
  661. # INPUT DATA
  662. # ============================================================
  663. df_use = df.copy()
  664. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  665. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  666. # ============================================================
  667. # KEEP ONLY LBD
  668. # ============================================================
  669. gdf = df_use[df_use["NPDx1"] == "lewy body disease"].copy()
  670. # ============================================================
  671. # STRUCTURES
  672. # ============================================================
  673. structures = [
  674. "hippocampus", "amygdala", "caudate",
  675. "putamen", "thalamus", "pallidum",
  676. "accumbens_area"
  677. ]
  678. palette = [
  679. "#56B4E9", "#E69F00", "#009E73",
  680. "#CC79A7", "#F0E442", "#0072B2",
  681. "#D55E00"
  682. ]
  683. # ============================================================
  684. # NORMALIZE POSTMORTEM VOLUMES
  685. # ============================================================
  686. for s in structures:
  687. pm_col = f"postmortem_{s}"
  688. if pm_col in gdf.columns and "antemortem_icv" in gdf.columns:
  689. gdf[f"{pm_col}_norm"] = gdf[pm_col] / gdf["antemortem_icv"]
  690. # ============================================================
  691. # FIND THALAMIC aSyn COLUMN
  692. # ============================================================
  693. thal_asyn_candidates = [c for c in gdf.columns if c.lower().startswith("ts")]
  694. thal_asyn_cols = [c for c in thal_asyn_candidates if "asyn" in c.lower()]
  695. if len(thal_asyn_cols) == 0:
  696. raise ValueError("No thalamic aSyn column found.")
  697. thal_asyn_col = thal_asyn_cols[0]
  698. print("Using thalamic aSyn column:", thal_asyn_col)
  699. # ============================================================
  700. # FIGURE SETUP: 3 x 3
  701. # ============================================================
  702. fig, axes = plt.subplots(3, 3, figsize=(16, 14), sharey=False)
  703. axes = axes.flatten()
  704. plt.subplots_adjust(hspace=0.5, wspace=0.4, top=0.88, bottom=0.08, left=0.08, right=0.98)
  705. all_stats = []
  706. all_pvals = []
  707. # ============================================================
  708. # COMPUTE STATS
  709. # ============================================================
  710. for idx, s in enumerate(structures):
  711. ax = axes[idx]
  712. post_col = f"postmortem_{s}_norm"
  713. if post_col not in gdf.columns:
  714. ax.text(0.5, 0.5, f"No volume\n{post_col}", ha="center", va="center", fontsize=12)
  715. ax.set_axis_off()
  716. continue
  717. needed_cols = [thal_asyn_col, post_col, "AgeatDeath", "Sex", "PMI", "Education"]
  718. missing_cols = [c for c in needed_cols if c not in gdf.columns]
  719. if missing_cols:
  720. ax.text(0.5, 0.5, "Missing covariates", ha="center", va="center", fontsize=12)
  721. ax.set_axis_off()
  722. continue
  723. d = gdf[needed_cols].dropna()
  724. if len(d) < 5:
  725. ax.text(0.5, 0.5, "N too small", ha="center", va="center", fontsize=12)
  726. ax.set_axis_off()
  727. continue
  728. res = pg.partial_corr(
  729. data=d,
  730. x=thal_asyn_col,
  731. y=post_col,
  732. covar=["AgeatDeath", "Sex", "PMI", "Education"],
  733. method="spearman"
  734. )
  735. r = res["r"].iloc[0]
  736. p = res["p-val"].iloc[0]
  737. all_stats.append({
  738. "idx": idx,
  739. "structure": s,
  740. "n": len(d),
  741. "r": r,
  742. "p_raw": p,
  743. "post_col": post_col,
  744. })
  745. all_pvals.append(p)
  746. # ============================================================
  747. # FDR CORRECTION
  748. # ============================================================
  749. if len(all_pvals) > 0:
  750. reject, p_corr = pg.multicomp(all_pvals, method="fdr_bh")
  751. else:
  752. reject, p_corr = [], []
  753. for i in range(len(all_stats)):
  754. all_stats[i]["p_fdr"] = p_corr[i]
  755. all_stats[i]["reject"] = reject[i]
  756. # ============================================================
  757. # PLOT
  758. # ============================================================
  759. for stat in all_stats:
  760. idx = stat["idx"]
  761. s = stat["structure"]
  762. r = stat["r"]
  763. p_fdr = stat["p_fdr"]
  764. post_col = stat["post_col"]
  765. ax = axes[idx]
  766. d = gdf[[thal_asyn_col, post_col, "AgeatDeath", "Sex", "PMI", "Education"]].dropna()
  767. sns.regplot(
  768. data=d,
  769. x=thal_asyn_col,
  770. y=post_col,
  771. scatter_kws=dict(alpha=0.7, s=45),
  772. line_kws=dict(color=palette[idx % len(palette)], lw=2),
  773. color=palette[idx % len(palette)],
  774. ax=ax
  775. )
  776. sig = "***" if p_fdr < 0.001 else "**" if p_fdr < 0.01 else "*" if p_fdr < 0.05 else ""
  777. title_name = s.replace("_", " ").title()
  778. ax.set_title(f"{title_name} (ρ={r:.2f}, p={stat['p_raw']:.3f}{sig})", fontsize=14, fontweight="bold")
  779. ax.set_xlabel("Thalamic α-synuclein", fontsize=13)
  780. ax.set_ylabel("Normalized volume", fontsize=13)
  781. ax.grid(True, linestyle=":", alpha=0.5)
  782. ax.margins(x=0.12)
  783. # ============================================================
  784. # TURN OFF UNUSED PANELS
  785. # ============================================================
  786. for j in range(len(structures), 9):
  787. axes[j].axis("off")
  788. # ============================================================
  789. # GLOBAL TITLE
  790. # ============================================================
  791. fig.suptitle(
  792. "LBD only: thalamic α-synuclein vs subcortical and limbic volumes",
  793. fontsize=20,
  794. y=0.97
  795. )
  796. plt.savefig(
  797. "LBD_thalamic_aSyn_vs_all_structures_3x3_with_accumbens.png",
  798. dpi=600,
  799. bbox_inches="tight"
  800. )
  801. plt.show()
  802. # %%
  803. """
  804. Fits a linear mixed-effects model testing associations between regional pathology markers and normalized postmortem subcortical volume across caudate, putamen, thalamus, and pallidum.
  805. The script builds a long-format dataframe with one row per subject–structure pair, z-scores the normalized volume outcome, includes Tau, TDP-43, and α-synuclein pathology scores plus covariates as fixed effects, and models participant-level random intercepts.
  806. """
  807. import pandas as pd
  808. import numpy as np
  809. import warnings
  810. import statsmodels.formula.api as smf
  811. warnings.filterwarnings("ignore")
  812. # ============================================================
  813. # INPUT DATA
  814. # ============================================================
  815. df_use = df.copy()
  816. # Required columns check
  817. SUBJECT_COL = "INDDID"
  818. needed_base = [SUBJECT_COL, "AgeatDeath", "Sex", "PMI", "Education"]
  819. missing = [c for c in needed_base if c not in df_use.columns]
  820. if missing:
  821. raise ValueError(f"Missing required columns: {missing}")
  822. # Clean / encode
  823. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  824. # ============================================================
  825. # ONLY THESE 4 STRUCTURES
  826. # ============================================================
  827. structures = ["caudate", "putamen", "thalamus", "pallidum"]
  828. # region/prefix map (your convention)
  829. region_map = {
  830. "caudate": "CP",
  831. "putamen": "CP",
  832. "thalamus": "TS",
  833. "pallidum": "GP",
  834. }
  835. # ============================================================
  836. # PRINT PATHOLOGY COLUMNS ENDING WITH "Tau"
  837. # (case-insensitive; also strips whitespace)
  838. # ============================================================
  839. tau_cols = [c for c in df_use.columns if str(c).strip().lower().endswith("tau")]
  840. print("\n===== Columns ending with 'Tau' (case-insensitive) =====")
  841. for c in tau_cols:
  842. print(c)
  843. # ============================================================
  844. # NORMALIZE POSTMORTEM VOLUMES (if not already)
  845. # expects: postmortem_<structure> and antemortem_icv
  846. # ============================================================
  847. if "antemortem_icv" not in df_use.columns:
  848. raise ValueError("Missing 'antemortem_icv' needed for volume normalization.")
  849. for s in structures:
  850. pm_col = f"postmortem_{s}"
  851. if pm_col not in df_use.columns:
  852. raise ValueError(f"Missing volume column: {pm_col}")
  853. df_use[f"{pm_col}_norm"] = df_use[pm_col] / df_use["antemortem_icv"]
  854. # ============================================================
  855. # Helper: pick pathology column for a structure + marker
  856. # Markers: "Tau", "TDP43", "aSyn"
  857. # Enforce CP split: caudate vs putamen by preferring colnames containing "caud" or "put"
  858. # ============================================================
  859. def pick_path_col(columns, prefix, marker, structure=None):
  860. """
  861. columns: list-like of df column names
  862. prefix: e.g. "CP", "TS", "GP"
  863. marker: "Tau" / "TDP43" / "aSyn"
  864. structure: optional; used only to enforce CP caudate vs putamen separation
  865. """
  866. cols = list(columns)
  867. prefix_l = prefix.lower()
  868. marker_l = marker.lower()
  869. # 1) candidate columns: start with prefix AND contain marker anywhere
  870. cand = [c for c in cols
  871. if str(c).lower().startswith(prefix_l) and marker_l in str(c).lower()]
  872. if not cand:
  873. return None
  874. # 2) enforce CP split for caudate vs putamen
  875. if prefix_l == "cp" and structure is not None:
  876. s_l = structure.lower()
  877. if s_l == "caudate":
  878. better = [c for c in cand if ("caud" in str(c).lower()) or ("caudate" in str(c).lower())]
  879. if better:
  880. cand = better
  881. if s_l == "putamen":
  882. better = [c for c in cand if ("put" in str(c).lower()) or ("putamen" in str(c).lower())]
  883. if better:
  884. cand = better
  885. # 3) prefer columns that END with marker (e.g., "...Tau") if available
  886. end_match = [c for c in cand if str(c).strip().lower().endswith(marker_l)]
  887. if end_match:
  888. cand = end_match
  889. # 4) deterministic pick (alphabetical)
  890. cand = sorted(cand)
  891. return cand[0]
  892. # ============================================================
  893. # BUILD LONG DATAFRAME
  894. # Each row = (INDDID, structure)
  895. # Columns: VolNorm, Tau, TDP43, aSyn + covariates
  896. # ============================================================
  897. rows = []
  898. chosen_cols = [] # debug table
  899. for s in structures:
  900. prefix = region_map[s]
  901. vol_col = f"postmortem_{s}_norm"
  902. tau_col = pick_path_col(df_use.columns, prefix, "Tau", structure=s)
  903. tdp_col = pick_path_col(df_use.columns, prefix, "TDP43", structure=s)
  904. asyn_col = pick_path_col(df_use.columns, prefix, "aSyn", structure=s)
  905. chosen_cols.append({
  906. "Structure": s,
  907. "Prefix": prefix,
  908. "VolCol": vol_col,
  909. "TauCol": tau_col,
  910. "TDP43Col": tdp_col,
  911. "aSynCol": asyn_col
  912. })
  913. # If any pathology marker is missing for this structure, we still build rows
  914. # but those rows will be dropped later if predictors are NaN.
  915. use_cols = [SUBJECT_COL, vol_col, "AgeatDeath", "Sex", "PMI", "Education"]
  916. rename_map = {SUBJECT_COL: "Subject", vol_col: "VolNorm"}
  917. if tau_col is not None:
  918. use_cols.append(tau_col)
  919. rename_map[tau_col] = "Tau"
  920. else:
  921. # create placeholder column later
  922. pass
  923. if tdp_col is not None:
  924. use_cols.append(tdp_col)
  925. rename_map[tdp_col] = "TDP43"
  926. if asyn_col is not None:
  927. use_cols.append(asyn_col)
  928. rename_map[asyn_col] = "aSyn"
  929. d = df_use[use_cols].copy().rename(columns=rename_map)
  930. d["Structure"] = s # kept for debugging only (NOT in model)
  931. # Ensure missing predictors exist as columns
  932. for pred in ["Tau", "TDP43", "aSyn"]:
  933. if pred not in d.columns:
  934. d[pred] = np.nan
  935. rows.append(d)
  936. long_df = pd.concat(rows, ignore_index=True)
  937. print("\n===== Chosen pathology columns per structure =====")
  938. chosen_cols_df = pd.DataFrame(chosen_cols)
  939. print(chosen_cols_df)
  940. # ============================================================
  941. # DROP MISSING + Z-SCORE OUTCOME ONLY
  942. # ============================================================
  943. # keep only complete cases for model variables
  944. model_cols = ["Subject", "VolNorm", "Tau", "TDP43", "aSyn", "Education", "AgeatDeath", "Sex", "PMI"]
  945. long_df = long_df.dropna(subset=model_cols).copy()
  946. # z-score outcome (global)
  947. long_df["VolNorm_z"] = (long_df["VolNorm"] - long_df["VolNorm"].mean()) / long_df["VolNorm"].std(ddof=0)
  948. # categorical subject
  949. long_df["Subject"] = long_df["Subject"].astype("category")
  950. print("\nN rows:", len(long_df), "| N participants:", long_df["Subject"].nunique())
  951. print(long_df[["Subject","Structure","VolNorm","VolNorm_z","Tau","TDP43","aSyn"]].head())
  952. # ============================================================
  953. # FIT MIXED EFFECTS MODEL
  954. # - No Structure term in formula (as you requested)
  955. # - Random intercept per participant
  956. # ============================================================
  957. formula = "VolNorm_z ~ Tau + TDP43 + aSyn + Education + AgeatDeath + Sex + PMI"
  958. print("\n===== Fitting MixedLM =====")
  959. print("Formula:", formula)
  960. m = smf.mixedlm(
  961. formula,
  962. data=long_df,
  963. groups=long_df["Subject"]
  964. ).fit(reml=False, method="lbfgs")
  965. print(m.summary())
  966. # ============================================================
  967. # OPTIONAL: show coefficients with more precision (helps when values look like 0.000)
  968. # ============================================================
  969. coefs = pd.DataFrame({
  970. "coef": m.params,
  971. "se": m.bse,
  972. "z": m.tvalues,
  973. "p": m.pvalues
  974. })
  975. print("\n===== Coefs with more precision =====")
  976. print(coefs.to_string(float_format=lambda x: f"{x:.6g}"))
  977. # %%
  978. """
  979. Fits disease-wise linear mixed-effects models testing whether the primary pathology burden for each diagnostic group is associated with ICV-normalized postmortem subcortical volume.
  980. The script builds a long-format dataset across caudate, putamen, thalamus, and pallidum, selects the disease-specific primary pathology marker, z-scores the volume outcome only, adjusts for education, age at death, sex, and PMI, and includes a random intercept for each participant.
  981. """
  982. import pandas as pd
  983. import numpy as np
  984. import warnings
  985. import statsmodels.formula.api as smf
  986. warnings.filterwarnings("ignore")
  987. # ============================================================
  988. # 0) LOAD
  989. # ============================================================
  990. df = pd.read_csv("")df_use = df.copy()
  991. # ============================================================
  992. # 1) FIND DISEASE GROUP COLUMN (you had KeyError for NPDx1)
  993. # We'll auto-detect from common options.
  994. # ============================================================
  995. CAND_GROUP_COLS = ["NPDx1", "Group", "group", "NPDx", "Dx", "Diagnosis", "diagnosis"]
  996. GROUP_COL = next((c for c in CAND_GROUP_COLS if c in df_use.columns), None)
  997. if GROUP_COL is None:
  998. raise ValueError(
  999. "❌ Could not find a disease group column. "
  1000. "Tried: " + ", ".join(CAND_GROUP_COLS) + "\n"
  1001. "Available columns (first 50):\n" + str(list(df_use.columns)[:50])
  1002. )
  1003. # standardize group strings
  1004. df_use[GROUP_COL] = df_use[GROUP_COL].astype(str).str.strip().str.lower()
  1005. # ============================================================
  1006. # 2) REQUIRED COLUMNS
  1007. # ============================================================
  1008. SUBJECT_COL = "INDDID"
  1009. if SUBJECT_COL not in df_use.columns:
  1010. raise ValueError(f"❌ Subject column '{SUBJECT_COL}' not found in df columns.")
  1011. # sex -> numeric codes
  1012. if "Sex" not in df_use.columns:
  1013. raise ValueError("❌ Missing required covariate column: Sex")
  1014. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  1015. for cov in ["AgeatDeath", "PMI", "Education"]:
  1016. if cov not in df_use.columns:
  1017. raise ValueError(f"❌ Missing required covariate column: {cov}")
  1018. # ICV needed for normalization
  1019. if "antemortem_icv" not in df_use.columns:
  1020. raise ValueError("❌ Missing 'antemortem_icv' needed for volume normalization.")
  1021. # ============================================================
  1022. # 3) STRUCTURES (ONLY these 4)
  1023. # ============================================================
  1024. structures = ["caudate", "putamen", "thalamus", "pallidum"]
  1025. # disease -> primary marker
  1026. # (same as your earlier mapping)
  1027. primary_marker_by_group = {
  1028. "alzheimer's disease": "Tau",
  1029. "lewy body disease": "aSyn",
  1030. "ftld-tdp": "TDP43",
  1031. "tauopathies": "Tau",
  1032. }
  1033. # region prefixes (your convention)
  1034. # NOTE: CP will be split caudate vs putamen using extra logic below
  1035. region_prefix = {
  1036. "caudate": "CP",
  1037. "putamen": "CP",
  1038. "thalamus": "TS",
  1039. "pallidum": "GP",
  1040. }
  1041. # ============================================================
  1042. # 4) NORMALIZE VOLUMES (RIGHT)
  1043. # ============================================================
  1044. for s in structures:
  1045. pm_col = f"postmortem_{s}"
  1046. if pm_col not in df_use.columns:
  1047. raise ValueError(f"❌ Missing volume column: {pm_col}")
  1048. df_use[f"{pm_col}_norm"] = df_use[pm_col] / df_use["antemortem_icv"]
  1049. # ============================================================
  1050. # 5) HELPERS: pick pathology column for a given structure + marker
  1051. # CP split: enforce caudate vs putamen when possible
  1052. # ============================================================
  1053. def pick_pathology_col(columns, structure, marker):
  1054. """
  1055. Returns the best-matching pathology column name or None.
  1056. marker in {"Tau","TDP43","aSyn"} (case-insensitive match).
  1057. """
  1058. cols = list(columns)
  1059. marker_l = marker.lower()
  1060. struct_l = structure.lower()
  1061. # candidate pool by prefix
  1062. pref = region_prefix[structure].lower()
  1063. pref_candidates = [c for c in cols if c.lower().startswith(pref)]
  1064. # keep only those with marker in name
  1065. marker_candidates = [c for c in pref_candidates if marker_l in c.lower()]
  1066. if len(marker_candidates) == 0:
  1067. return None
  1068. # --- CP split enforcement ---
  1069. # try to choose a caudate-specific vs putamen-specific CP column
  1070. if structure == "caudate":
  1071. # prefer explicit caudate hints
  1072. caud_pref = [c for c in marker_candidates if ("caud" in c.lower() or "caudate" in c.lower())]
  1073. if len(caud_pref) > 0:
  1074. return caud_pref[0]
  1075. if structure == "putamen":
  1076. put_pref = [c for c in marker_candidates if ("put" in c.lower() or "putamen" in c.lower())]
  1077. if len(put_pref) > 0:
  1078. return put_pref[0]
  1079. # fallback: take the first marker match within prefix
  1080. return marker_candidates[0]
  1081. # ============================================================
  1082. # 6) BUILD LONG DF: one row per (Subject, Structure)
  1083. # PrimaryPath depends on disease group
  1084. # ============================================================
  1085. rows = []
  1086. chosen_cols_log = [] # track what got picked
  1087. for group_name, marker in primary_marker_by_group.items():
  1088. gdf = df_use[df_use[GROUP_COL] == group_name].copy()
  1089. if gdf.empty:
  1090. continue
  1091. for s in structures:
  1092. vol_col = f"postmortem_{s}_norm"
  1093. path_col = pick_pathology_col(gdf.columns, structure=s, marker=marker)
  1094. chosen_cols_log.append((group_name, s, marker, path_col, vol_col))
  1095. if path_col is None:
  1096. continue
  1097. d = gdf[[SUBJECT_COL, path_col, vol_col, "AgeatDeath", "Sex", "PMI", "Education"]].copy()
  1098. d = d.rename(columns={
  1099. SUBJECT_COL: "Subject",
  1100. path_col: "PrimaryPath",
  1101. vol_col: "VolNorm",
  1102. })
  1103. d["Disease"] = group_name # keep disease label
  1104. d["Structure"] = s # kept for debugging (NOT in formula)
  1105. rows.append(d)
  1106. if len(rows) == 0:
  1107. raise ValueError("❌ No long-format rows created. Check pathology column naming / prefixes / markers.")
  1108. mB_df = pd.concat(rows, ignore_index=True)
  1109. # drop NAs & reset index to avoid statsmodels IndexError
  1110. need = ["Subject", "PrimaryPath", "VolNorm", "AgeatDeath", "Sex", "PMI", "Education", "Disease"]
  1111. mB_df = mB_df.dropna(subset=need).reset_index(drop=True)
  1112. # Z-score OUTCOME only (across the whole dataset used for each disease model)
  1113. # (You asked: don't z-score predictors)
  1114. mB_df["VolNorm_z"] = (mB_df["VolNorm"] - mB_df["VolNorm"].mean()) / mB_df["VolNorm"].std(ddof=0)
  1115. # cast subject categorical
  1116. mB_df["Subject"] = mB_df["Subject"].astype("category")
  1117. # ============================================================
  1118. # 7) DEBUG OUTPUTS YOU ASKED FOR
  1119. # ============================================================
  1120. print("\n===== GROUP COLUMN USED =====")
  1121. print("GROUP_COL =", GROUP_COL)
  1122. print("\n===== Chosen pathology columns (first 30 rows) =====")
  1123. log_df = pd.DataFrame(chosen_cols_log, columns=["Disease", "Structure", "Marker", "ChosenPathCol", "VolCol"])
  1124. print(log_df.head(30))
  1125. print("\n===== PrimaryPath value counts (including NA) =====")
  1126. print(mB_df["PrimaryPath"].value_counts(dropna=False))
  1127. print("\n===== Preview mB_df =====")
  1128. print(mB_df.head())
  1129. # ============================================================
  1130. # 8) FIT 4 DISEASE-WISE LME MODELS (PRIMARY PATH ONLY)
  1131. # No Structure term in formula (as requested)
  1132. # ============================================================
  1133. formula = "VolNorm_z ~ PrimaryPath + Education + AgeatDeath + Sex + PMI"
  1134. print("\n===== Formula =====")
  1135. print(formula)
  1136. models = {}
  1137. for disease in primary_marker_by_group.keys():
  1138. ddf = mB_df[mB_df["Disease"] == disease].copy()
  1139. # sanity checks
  1140. n_sub = ddf["Subject"].nunique()
  1141. n_rows = len(ddf)
  1142. print(f"\n------------------------------\nDISEASE: {disease}\nrows={n_rows} | subjects={n_sub}")
  1143. if n_rows < 20 or n_sub < 5:
  1144. print("⚠️ Skipping (too few rows or subjects).")
  1145. continue
  1146. # Important: reset index again per subset (prevents out-of-bounds indexing)
  1147. ddf = ddf.reset_index(drop=True)
  1148. # Fit random-intercept model
  1149. fit = smf.mixedlm(
  1150. formula=formula,
  1151. data=ddf,
  1152. groups=ddf["Subject"]
  1153. ).fit(reml=False, method="lbfgs")
  1154. models[disease] = fit
  1155. print(fit.summary())
  1156. # ============================================================
  1157. # 9) (OPTIONAL) QUICK: show two random subjects' rows (debug)
  1158. # ============================================================
  1159. rng = np.random.default_rng(0)
  1160. subj_two = rng.choice(mB_df["Subject"].astype(str).unique(), size=2, replace=False)
  1161. print("\n===== Two random subjects =====")
  1162. print("Subjects:", subj_two.tolist())
  1163. print(mB_df[mB_df["Subject"].astype(str).isin(subj_two)].sort_values(["Subject", "Structure"]))
  1164. # %%
  1165. """
  1166. Fits disease-wise polypathology linear mixed-effects models testing whether Tau, TDP-43, and α-synuclein pathology are associated with ICV-normalized postmortem subcortical volume.
  1167. The script builds a long-format dataset across caudate, putamen, thalamus, and pallidum, z-scores the normalized volume outcome only, adjusts for age at death, sex, PMI, and education, and includes participant-level random intercepts while fitting separate models within each diagnostic group.
  1168. """
  1169. import pandas as pd
  1170. import numpy as np
  1171. import re
  1172. import warnings
  1173. import statsmodels.formula.api as smf
  1174. warnings.filterwarnings("ignore")
  1175. # ============================================================
  1176. # 0) LOAD DATA
  1177. # ============================================================
  1178. df = pd.read_csv("")print("Loaded:", df.shape)
  1179. print(df.head())
  1180. # ============================================================
  1181. # 1) CONFIG
  1182. # ============================================================
  1183. SUBJECT_COL = "INDDID" # confirmed by you
  1184. DX_COL = "NPDx1" # if your file doesn't have NPDx1, see fallback below
  1185. # 4 subcortical structures you want
  1186. structures = ["caudate", "putamen", "thalamus", "pallidum"]
  1187. # region prefixes you were using
  1188. # NOTE: caudate/putamen share CP prefix in your data
  1189. region_prefix = {
  1190. "caudate": "CP",
  1191. "putamen": "CP",
  1192. "thalamus": "TS",
  1193. "pallidum": "GP",
  1194. }
  1195. # marker suffixes in your sheet
  1196. markers = ["Tau", "TDP43", "aSyn"]
  1197. # outcome columns (normalized volumes)
  1198. VOL_BASE = {s: f"postmortem_{s}" for s in structures}
  1199. ICV_COL = "antemortem_icv"
  1200. # covariates
  1201. COVARS = ["AgeatDeath", "Sex", "PMI", "Education"]
  1202. # diseases of interest (edit these to match your values)
  1203. disease_list = [
  1204. "alzheimer's disease",
  1205. "lewy body disease",
  1206. "ftld-tdp",
  1207. "tauopathies",
  1208. ]
  1209. # ============================================================
  1210. # 2) SAFETY / CLEANING
  1211. # ============================================================
  1212. df_use = df.copy()
  1213. # --- Subject ID must exist ---
  1214. if SUBJECT_COL not in df_use.columns:
  1215. raise ValueError(f"❌ Missing subject column '{SUBJECT_COL}' in dataframe.")
  1216. # --- Dx column fallback (if NPDx1 not present) ---
  1217. if DX_COL not in df_use.columns:
  1218. # If your file uses a different column name, set DX_COL above.
  1219. # Otherwise, try a few common alternatives:
  1220. for alt in ["Group", "NPDx", "Dx", "Diagnosis", "NPDx1_clean"]:
  1221. if alt in df_use.columns:
  1222. DX_COL = alt
  1223. print(f"⚠️ Using '{DX_COL}' as diagnosis column (fallback).")
  1224. break
  1225. else:
  1226. raise ValueError("❌ Could not find a diagnosis column. Set DX_COL to the correct column name.")
  1227. # Normalize dx strings
  1228. df_use[DX_COL] = df_use[DX_COL].astype(str).str.strip().str.lower()
  1229. # Sex: make numeric scalar codes (avoid patsy “>1-dimensional” issues)
  1230. # If Sex is already numeric 0/1, this will keep it numeric.
  1231. if df_use["Sex"].dtype.name == "category" or df_use["Sex"].dtype == object:
  1232. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  1233. # Force numeric covariates (this prevents PatsyError if any are weird objects/arrays)
  1234. for c in ["AgeatDeath", "PMI", "Education"]:
  1235. if c in df_use.columns:
  1236. df_use[c] = pd.to_numeric(df_use[c], errors="coerce")
  1237. # ============================================================
  1238. # 3) PRINT “Tau columns” (names ending with Tau)
  1239. # ============================================================
  1240. tau_cols = [c for c in df_use.columns if re.search(r"tau\s*$", str(c), flags=re.IGNORECASE)]
  1241. print("\n===== Columns ending with 'Tau' =====")
  1242. print(tau_cols[:200])
  1243. print("Count:", len(tau_cols))
  1244. # ============================================================
  1245. # 4) NORMALIZE VOLUMES and BUILD LONG DF
  1246. # ============================================================
  1247. # create normalized volume columns
  1248. for s in structures:
  1249. vol_col = VOL_BASE[s]
  1250. if vol_col in df_use.columns and ICV_COL in df_use.columns:
  1251. df_use[f"{vol_col}_norm"] = df_use[vol_col] / df_use[ICV_COL]
  1252. else:
  1253. print(f"⚠️ Missing {vol_col} or {ICV_COL}; cannot normalize for {s}.")
  1254. def choose_path_col(columns, prefix, marker, structure):
  1255. """
  1256. Choose pathology column for a given (prefix, marker, structure).
  1257. - primary match: startswith(prefix) AND endswith(marker)
  1258. - CP special case: prefer columns containing 'caud' for caudate and 'put' for putamen
  1259. """
  1260. cols = list(columns)
  1261. # candidate columns by prefix
  1262. pref = [c for c in cols if str(c).lower().startswith(prefix.lower())]
  1263. if not pref:
  1264. return None
  1265. # keep those ending with marker (Tau / TDP43 / aSyn)
  1266. # (ending-with constraint as you requested)
  1267. mk = [c for c in pref if re.search(rf"{re.escape(marker)}\s*$", str(c), flags=re.IGNORECASE)]
  1268. if not mk:
  1269. # if none strictly end with marker, loosen slightly (marker anywhere)
  1270. mk = [c for c in pref if marker.lower() in str(c).lower()]
  1271. if not mk:
  1272. return None
  1273. # enforce CP split caudate vs putamen
  1274. if prefix.lower() == "cp":
  1275. if structure.lower() == "caudate":
  1276. # prefer columns mentioning caudate-ish strings
  1277. for key in ["caud", "caudate", "head"]:
  1278. hit = [c for c in mk if key in str(c).lower()]
  1279. if hit:
  1280. return hit[0]
  1281. if structure.lower() == "putamen":
  1282. for key in ["put", "putamen"]:
  1283. hit = [c for c in mk if key in str(c).lower()]
  1284. if hit:
  1285. return hit[0]
  1286. # fallback: if no structure-specific keyword, still return first match
  1287. return mk[0]
  1288. # non-CP: just first best match
  1289. return mk[0]
  1290. rows = []
  1291. debug_choices = []
  1292. for s in structures:
  1293. vol_norm = f"{VOL_BASE[s]}_norm"
  1294. if vol_norm not in df_use.columns:
  1295. continue
  1296. prefix = region_prefix[s]
  1297. # choose columns for each marker
  1298. col_tau = choose_path_col(df_use.columns, prefix, "Tau", s)
  1299. col_tdp = choose_path_col(df_use.columns, prefix, "TDP43", s)
  1300. col_asyn = choose_path_col(df_use.columns, prefix, "aSyn", s)
  1301. debug_choices.append((s, prefix, col_tau, col_tdp, col_asyn, vol_norm))
  1302. keep = [SUBJECT_COL, DX_COL, vol_norm] + COVARS
  1303. # add pathology cols if exist
  1304. for cc in [col_tau, col_tdp, col_asyn]:
  1305. if cc is not None:
  1306. keep.append(cc)
  1307. d = df_use[keep].copy()
  1308. # rename to standard names
  1309. rename_map = {SUBJECT_COL: "Subject", DX_COL: "Dx", vol_norm: "VolNorm"}
  1310. if col_tau is not None: rename_map[col_tau] = "Tau"
  1311. if col_tdp is not None: rename_map[col_tdp] = "TDP43"
  1312. if col_asyn is not None: rename_map[col_asyn] = "aSyn"
  1313. d = d.rename(columns=rename_map)
  1314. d["Structure"] = s
  1315. # ensure pathology columns exist (if missing, create with NaN so formula is stable)
  1316. for m in ["Tau", "TDP43", "aSyn"]:
  1317. if m not in d.columns:
  1318. d[m] = np.nan
  1319. rows.append(d)
  1320. long_df = pd.concat(rows, ignore_index=True)
  1321. print("\n===== Column choices per structure =====")
  1322. debug_df = pd.DataFrame(debug_choices, columns=["Structure", "Prefix", "Tau_col", "TDP43_col", "aSyn_col", "VolNorm_col"])
  1323. print(debug_df)
  1324. # ============================================================
  1325. # 5) CLEAN LONG DF + Z-SCORE OUTCOME ONLY (as you requested)
  1326. # ============================================================
  1327. # keep only diseases we care about (optional but recommended)
  1328. long_df["Dx"] = long_df["Dx"].astype(str).str.strip().str.lower()
  1329. long_df = long_df[long_df["Dx"].isin(disease_list)].copy()
  1330. # force numeric predictors (avoid PatsyError)
  1331. for m in ["Tau", "TDP43", "aSyn"]:
  1332. long_df[m] = pd.to_numeric(long_df[m], errors="coerce")
  1333. for c in ["AgeatDeath", "PMI", "Education", "VolNorm"]:
  1334. long_df[c] = pd.to_numeric(long_df[c], errors="coerce")
  1335. # drop rows missing essential fields (but allow missing pathology in some rows; you can tighten later)
  1336. essential = ["Subject", "Dx", "VolNorm"] + COVARS
  1337. long_df = long_df.dropna(subset=essential).copy()
  1338. # z-score OUTCOME only (within entire stacked dataset)
  1339. long_df["VolNorm_z"] = (long_df["VolNorm"] - long_df["VolNorm"].mean()) / long_df["VolNorm"].std(ddof=0)
  1340. # IMPORTANT for statsmodels MixedLM: reset index AFTER drops to avoid your IndexError
  1341. long_df = long_df.reset_index(drop=True)
  1342. print("\n===== LONG DF summary =====")
  1343. print("Rows:", len(long_df), "| Subjects:", long_df["Subject"].nunique())
  1344. print(long_df[["Subject", "Dx", "Structure", "VolNorm", "VolNorm_z", "Tau", "TDP43", "aSyn"]].head())
  1345. # ============================================================
  1346. # 6) QUICK DEBUG HELPERS (you asked for these)
  1347. # ============================================================
  1348. # Show any two random subjects (raw rows)
  1349. two_subj = np.random.choice(long_df["Subject"].unique(), size=min(2, long_df["Subject"].nunique()), replace=False)
  1350. print("\n===== Two random subjects =====")
  1351. print(two_subj)
  1352. print(long_df[long_df["Subject"].isin(two_subj)][
  1353. ["Subject", "Dx", "Structure", "VolNorm", "VolNorm_z", "Tau", "TDP43", "aSyn", "AgeatDeath", "Sex", "PMI", "Education"]
  1354. ].sort_values(["Subject", "Structure"]))
  1355. # Check PrimaryPath-style column completeness (here: Tau/TDP43/aSyn)
  1356. print("\n===== Value counts (including NaN) for Tau / TDP43 / aSyn =====")
  1357. print("Tau:")
  1358. print(long_df["Tau"].isna().value_counts(dropna=False))
  1359. print("TDP43:")
  1360. print(long_df["TDP43"].isna().value_counts(dropna=False))
  1361. print("aSyn:")
  1362. print(long_df["aSyn"].isna().value_counts(dropna=False))
  1363. # ============================================================
  1364. # 7) FIT POLYPATHOLOGY LME PER DISEASE (4 models)
  1365. # - No Structure term in formula (you explicitly asked)
  1366. # - Random intercept per Subject
  1367. # ============================================================
  1368. # NOTE: If some diseases have pathology columns totally missing (all NaN),
  1369. # MixedLM will fail. We handle this by dropping predictors that are all-NaN within each disease.
  1370. base_cov = "AgeatDeath + Sex + PMI + Education"
  1371. base_paths = ["Tau", "TDP43", "aSyn"]
  1372. results = {}
  1373. for dx in disease_list:
  1374. dxd = long_df[long_df["Dx"] == dx].copy()
  1375. # pick which pathology predictors are usable in this disease subset
  1376. usable = [p for p in base_paths if dxd[p].notna().sum() > 5] # require at least 5 non-NaN points
  1377. if len(usable) == 0:
  1378. print(f"\n⚠️ Skipping {dx}: no pathology predictors have enough non-missing values.")
  1379. continue
  1380. # drop rows with missing in *usable* predictors (model needs complete cases)
  1381. model_df = dxd.dropna(subset=usable + COVARS + ["VolNorm_z", "Subject"]).copy()
  1382. # also reset index to avoid IndexError in statsmodels
  1383. model_df = model_df.reset_index(drop=True)
  1384. if model_df["Subject"].nunique() < 3 or len(model_df) < 20:
  1385. print(f"\n⚠️ Skipping {dx}: too few subjects/rows after filtering. "
  1386. f"Rows={len(model_df)}, Subjects={model_df['Subject'].nunique()}")
  1387. continue
  1388. # build formula (NO Structure)
  1389. path_term = " + ".join(usable)
  1390. formula = f"VolNorm_z ~ {path_term} + {base_cov}"
  1391. print("\n" + "="*90)
  1392. print(f"Fitting polypathology LME for: {dx}")
  1393. print("Rows:", len(model_df), "| Subjects:", model_df["Subject"].nunique())
  1394. print("Formula:", formula)
  1395. # Fit
  1396. try:
  1397. m = smf.mixedlm(
  1398. formula=formula,
  1399. data=model_df,
  1400. groups=model_df["Subject"]
  1401. ).fit(reml=False, method="lbfgs")
  1402. print(m.summary())
  1403. results[dx] = m
  1404. except Exception as e:
  1405. print(f"❌ Model failed for {dx}: {e}")
  1406. print("\nDone. Models fit:", list(results.keys()))
  1407. ##########################################################################################
  1408. # %%
  1409. ##########################################################################################
  1410. ########## Addtional markers
  1411. ##########################################################################################
  1412. # %%
  1413. """
  1414. Generates separate publication-style figures for partial Spearman correlations between global pathology severity markers and ICV-normalized postmortem limbic/subcortical volumes.
  1415. For each pathology marker, diagnostic group, and structure, the script adjusts for age at death, sex, PMI, and education, applies FDR correction across structures within each group, annotates significance with superscript asterisks, and saves both PNG and PDF outputs.
  1416. """
  1417. import pandas as pd
  1418. import numpy as np
  1419. import seaborn as sns
  1420. import matplotlib.pyplot as plt
  1421. import matplotlib
  1422. import pingouin as pg
  1423. from matplotlib.backends.backend_pdf import PdfPages
  1424. matplotlib.rcParams['font.family'] = 'DejaVu Sans'
  1425. sns.set(style="whitegrid")
  1426. np.seterr(divide='ignore', invalid='ignore')
  1427. ##########################################################################################
  1428. # DATA PREP
  1429. ##########################################################################################
  1430. df_use = df.copy()
  1431. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  1432. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  1433. ##########################################################################################
  1434. # STRUCTURES + NORMALIZATION
  1435. ##########################################################################################
  1436. structures = ["hippocampus", "amygdala", "caudate",
  1437. "putamen", "thalamus", "pallidum", "accumbens_area"]
  1438. pretty_structure = {
  1439. "hippocampus": "Hippocampus",
  1440. "amygdala": "Amygdala",
  1441. "caudate": "Caudate",
  1442. "putamen": "Putamen",
  1443. "thalamus": "Thalamus",
  1444. "pallidum": "Pallidum",
  1445. "accumbens_area": "Accumbens area"
  1446. }
  1447. for s in structures:
  1448. pm = f"postmortem_{s}"
  1449. if pm in df_use.columns:
  1450. df_use[f"{pm}_norm"] = df_use[pm] / df_use["antemortem_icv"]
  1451. ##########################################################################################
  1452. # PATHOLOGY MARKERS
  1453. ##########################################################################################
  1454. markers = ["ABeta", "CERAD", "Braak06", "CAA",
  1455. "Arteriolosclerosis", "Atherosclerosis"]
  1456. pretty_marker = {
  1457. "ABeta": "Aβ (Amyloid-β)",
  1458. "CERAD": "CERAD plaques",
  1459. "Braak06": "Braak stage",
  1460. "CAA": "CAA",
  1461. "Arteriolosclerosis": "Arteriolosclerosis",
  1462. "Atherosclerosis": "Atherosclerosis"
  1463. }
  1464. severity_map = {
  1465. "0": 0, "None": 0, "None/Normal": 0,
  1466. "1": 1, "Mild": 1,
  1467. "2": 2, "Moderate": 2,
  1468. "3": 3, "Severe": 3
  1469. }
  1470. for m in markers:
  1471. if m in df_use.columns:
  1472. df_use[m] = (
  1473. df_use[m]
  1474. .astype(str)
  1475. .replace("Unknown", np.nan)
  1476. .replace(severity_map)
  1477. .replace("nan", np.nan)
  1478. )
  1479. df_use[m] = pd.to_numeric(df_use[m], errors="coerce")
  1480. ##########################################################################################
  1481. # GROUPS + COLORS
  1482. ##########################################################################################
  1483. groups = ["alzheimer's disease", "lewy body disease", "ftld-tdp", "tauopathies"]
  1484. pretty_group = {
  1485. "alzheimer's disease": "AD",
  1486. "lewy body disease": "LBD",
  1487. "ftld-tdp": "FTLD-TDP",
  1488. "tauopathies": "Tauopathies"
  1489. }
  1490. group_color = {
  1491. "alzheimer's disease": "#4C72B0",
  1492. "lewy body disease": "#DD8452",
  1493. "ftld-tdp": "#55A868",
  1494. "tauopathies": "#CBAF00"
  1495. }
  1496. ##########################################################################################
  1497. # SIGNIFICANCE → SUPERSCRIPT ASTERISKS
  1498. ##########################################################################################
  1499. def p_to_superscript(p_fdr):
  1500. if p_fdr < 0.001:
  1501. return "$^{***}$"
  1502. elif p_fdr < 0.01:
  1503. return "$^{**}$"
  1504. elif p_fdr < 0.05:
  1505. return "$^{*}$"
  1506. else:
  1507. return ""
  1508. ##########################################################################################
  1509. # MAIN LOOP — ONE FIGURE PER MARKER
  1510. ##########################################################################################
  1511. for marker in markers:
  1512. fig, axes = plt.subplots(
  1513. len(structures), len(groups),
  1514. figsize=(4.2 * len(groups), 3.0 * len(structures)),
  1515. sharex=False, sharey=False
  1516. )
  1517. # Compute for each disease group
  1518. for col, g in enumerate(groups):
  1519. gdf = df_use[df_use["NPDx1"] == g]
  1520. # FIRST PASS: collect all raw p-values for FDR correction
  1521. raw_pvals = []
  1522. for s in structures:
  1523. post_col = f"postmortem_{s}_norm"
  1524. if marker not in gdf.columns or post_col not in gdf.columns:
  1525. raw_pvals.append(np.nan)
  1526. continue
  1527. d = gdf[[marker, post_col, "AgeatDeath", "Sex", "PMI", "Education"]]
  1528. d = d.apply(pd.to_numeric, errors="coerce").dropna()
  1529. if len(d) < 5:
  1530. raw_pvals.append(np.nan)
  1531. continue
  1532. res = pg.partial_corr(
  1533. data=d, x=marker, y=post_col,
  1534. covar=["AgeatDeath", "Sex", "PMI", "Education"],
  1535. method="spearman"
  1536. )
  1537. raw_pvals.append(res["p-val"].iloc[0])
  1538. # FDR
  1539. valid_mask = ~pd.isna(raw_pvals)
  1540. valid_p = np.array(raw_pvals)[valid_mask]
  1541. if len(valid_p) > 0:
  1542. _, p_fdr_valid = pg.multicomp(valid_p, method="fdr_bh")
  1543. else:
  1544. p_fdr_valid = []
  1545. # Expand back
  1546. p_fdr_full = []
  1547. idx = 0
  1548. for v in valid_mask:
  1549. if v:
  1550. p_fdr_full.append(p_fdr_valid[idx])
  1551. idx += 1
  1552. else:
  1553. p_fdr_full.append(np.nan)
  1554. # SECOND PASS: plotting
  1555. for row, s in enumerate(structures):
  1556. ax = axes[row, col]
  1557. post_col = f"postmortem_{s}_norm"
  1558. # Remove default labels
  1559. ax.set_ylabel("")
  1560. # Set clean y-label only for left-most column
  1561. if col == 0:
  1562. ax.set_ylabel(pretty_structure[s], fontsize=13)
  1563. # Prepare data
  1564. if marker not in gdf.columns or post_col not in gdf.columns:
  1565. ax.text(0.5, 0.5, "No data", ha="center", va="center")
  1566. ax.set_xticks([])
  1567. ax.set_yticks([])
  1568. continue
  1569. d = gdf[[marker, post_col, "AgeatDeath", "Sex", "PMI", "Education"]]
  1570. d = d.apply(pd.to_numeric, errors="coerce").dropna()
  1571. if len(d) < 5:
  1572. ax.text(0.5, 0.5, "N too small", ha="center", va="center")
  1573. ax.set_xticks([])
  1574. ax.set_yticks([])
  1575. continue
  1576. # Partial Spearman
  1577. res = pg.partial_corr(
  1578. data=d, x=marker, y=post_col,
  1579. covar=["AgeatDeath", "Sex", "PMI", "Education"],
  1580. method="spearman"
  1581. )
  1582. r = res["r"].iloc[0]
  1583. p_raw = res["p-val"].iloc[0]
  1584. p_fdr = p_fdr_full[row]
  1585. sup = p_to_superscript(p_fdr)
  1586. # Scatter + line
  1587. sns.regplot(
  1588. data=d,
  1589. x=marker,
  1590. y=post_col,
  1591. scatter_kws=dict(color=group_color[g], s=50, alpha=0.75),
  1592. line_kws=dict(color=group_color[g], lw=2.3, alpha=0.9),
  1593. ax=ax
  1594. )
  1595. # Reset y-label after regplot overwrites it
  1596. ax.set_ylabel("")
  1597. if col == 0:
  1598. ax.set_ylabel(pretty_structure[s], fontsize=13)
  1599. # Pretty x-axis label
  1600. if row == len(structures) - 1:
  1601. ax.set_xlabel(pretty_marker[marker], fontsize=12)
  1602. else:
  1603. ax.set_xlabel("")
  1604. # Title with superscript asterisks
  1605. ax.set_title(
  1606. f"{pretty_group[g]} (ρ = {r:.2f}; p = {p_raw:.3f}{sup})",
  1607. fontsize=12, pad=6
  1608. )
  1609. ax.grid(True, linestyle=":", alpha=0.5)
  1610. # Figure title
  1611. plt.suptitle(
  1612. f"Partial Spearman correlations — {pretty_marker[marker]}",
  1613. fontsize=18, weight="bold"
  1614. )
  1615. plt.tight_layout(rect=[0, 0, 1, 0.95])
  1616. # SAVE PNG
  1617. png_name = f"Postmortem_Partial_Spearman_{marker}.png"
  1618. plt.savefig(png_name, dpi=300, bbox_inches="tight")
  1619. print(f"SAVED PNG: {png_name}")
  1620. # SAVE PDF
  1621. pdf_name = f"Postmortem_Partial_Spearman_{marker}.pdf"
  1622. with PdfPages(pdf_name) as pdf:
  1623. pdf.savefig(fig, bbox_inches="tight")
  1624. print(f"SAVED PDF: {pdf_name}")
  1625. plt.show()
  1626. print("\n✔✔✔ ALL FIGURES SAVED (PNG 300dpi + PDF)")
  1627. # %%
  1628. """
  1629. Computes partial Spearman correlations between global pathology/vascular severity markers and ICV-normalized postmortem limbic/subcortical volumes across diagnostic groups.
  1630. The script adjusts for age at death, sex, PMI, and education, applies FDR correction across structures within each marker–group comparison, and visualizes the resulting correlation coefficients as clean 3×3 heatmaps with significance annotations and a separate colorbar legend.
  1631. """
  1632. import pandas as pd, numpy as np, seaborn as sns, matplotlib.pyplot as plt, pingouin as pg, warnings
  1633. from matplotlib.transforms import Bbox
  1634. import matplotlib
  1635. matplotlib.rcParams['font.family'] = 'DejaVu Sans'
  1636. warnings.filterwarnings("ignore", category=RuntimeWarning)
  1637. np.seterr(divide='ignore', invalid='ignore')
  1638. # ---------------------------------------------------------------------
  1639. # USE df_use EXACTLY AS IS (NO RELABELING OF GROUPS)
  1640. # ---------------------------------------------------------------------
  1641. df_use = df.copy()
  1642. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  1643. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  1644. # ---------------------------------------------------------------------
  1645. # Structures
  1646. # ---------------------------------------------------------------------
  1647. structures = ["hippocampus", "amygdala", "caudate",
  1648. "putamen", "thalamus", "pallidum", "accumbens_area"]
  1649. # Normalize volumes
  1650. for s in structures:
  1651. pm = f"postmortem_{s}"
  1652. if pm in df_use.columns:
  1653. df_use[f"{pm}_norm"] = df_use[pm] / df_use["antemortem_icv"]
  1654. # ---------------------------------------------------------------------
  1655. # Pathology markers
  1656. # ---------------------------------------------------------------------
  1657. pathology_markers = [
  1658. "ABeta", "Braak06", "CERAD", "CAA",
  1659. "Arteriolosclerosis", "Atherosclerosis"
  1660. ]
  1661. severity_map = {
  1662. "0": 0, "None": 0, "None/Normal": 0,
  1663. "1": 1, "Mild": 1,
  1664. "2": 2, "Moderate": 2,
  1665. "3": 3, "Severe": 3
  1666. }
  1667. for m in pathology_markers:
  1668. if m in df_use.columns:
  1669. df_use[m] = (
  1670. df_use[m].astype(str)
  1671. .replace("Unknown", np.nan)
  1672. .replace(severity_map)
  1673. .replace("nan", np.nan)
  1674. )
  1675. df_use[m] = pd.to_numeric(df_use[m], errors="coerce")
  1676. # ---------------------------------------------------------------------
  1677. # Settings
  1678. # ---------------------------------------------------------------------
  1679. groups = ["alzheimer's disease", "lewy body disease", "ftld-tdp", "tauopathies"]
  1680. group_labels = ["AD", "LBD", "FTLD-TDP", "Tauopathies"]
  1681. covars = ["AgeatDeath", "Sex", "PMI", "Education"]
  1682. sns.set(style="white", context="talk")
  1683. # ---------------------------------------------------------------------
  1684. # Compute partial correlations
  1685. # ---------------------------------------------------------------------
  1686. results_all = []
  1687. for marker in pathology_markers:
  1688. for g in groups:
  1689. sub = df_use[df_use["NPDx1"] == g].copy()
  1690. if sub.empty:
  1691. continue
  1692. all_rho, all_p = [], []
  1693. for s in structures:
  1694. post_col = f"postmortem_{s}_norm"
  1695. if post_col not in sub.columns:
  1696. all_rho.append(np.nan)
  1697. all_p.append(np.nan)
  1698. continue
  1699. cols = [marker, post_col] + covars
  1700. d = sub[cols].apply(pd.to_numeric, errors="coerce").dropna()
  1701. if len(d) < 5:
  1702. all_rho.append(np.nan)
  1703. all_p.append(np.nan)
  1704. continue
  1705. res = pg.partial_corr(
  1706. data=d, x=marker, y=post_col,
  1707. covar=covars, method="spearman"
  1708. )
  1709. rho = res["r"].iloc[0]
  1710. p = res["p-val"].iloc[0]
  1711. all_rho.append(rho)
  1712. all_p.append(p)
  1713. valid = ~pd.isna(all_p)
  1714. pvals = np.array(all_p)[valid]
  1715. if len(pvals) > 0:
  1716. _, p_corr = pg.multicomp(pvals, method="fdr_bh")
  1717. else:
  1718. p_corr = []
  1719. idx = 0
  1720. for s, rho, p in zip(structures, all_rho, all_p):
  1721. if not pd.isna(p):
  1722. pFDR = p_corr[idx]
  1723. idx += 1
  1724. else:
  1725. pFDR = np.nan
  1726. results_all.append({
  1727. "Marker": marker,
  1728. "Group": g,
  1729. "Structure": s,
  1730. "rho": rho,
  1731. "p_val": p,
  1732. "p_FDR": pFDR
  1733. })
  1734. results_master_df = pd.DataFrame(results_all)
  1735. print("\n✅ Partial Spearman correlations computed + FDR corrected.\n")
  1736. # ---------------------------------------------------------------------
  1737. # Plotting: 3×3 heatmaps
  1738. # ---------------------------------------------------------------------
  1739. sns.set(style="white", context="talk")
  1740. cmap = sns.diverging_palette(280, 150, s=90, l=60, as_cmap=True)
  1741. vmin, vmax = -0.6, 0.6
  1742. def fdr_star(p):
  1743. if p < 0.001: return "***"
  1744. elif p < 0.01: return "**"
  1745. elif p < 0.05: return "*"
  1746. return ""
  1747. titles = {
  1748. "ABeta": "Aβ (Amyloid-β)",
  1749. "Braak06": "Braak stage",
  1750. "CERAD": "CERAD plaques",
  1751. "CAA": "CAA",
  1752. "Arteriolosclerosis": "Arteriolosclerosis",
  1753. "Atherosclerosis": "Atherosclerosis"
  1754. }
  1755. structure_order = structures
  1756. fig, axes = plt.subplots(3, 3, figsize=(18, 18))
  1757. axes = axes.flatten()
  1758. for i, marker in enumerate(pathology_markers):
  1759. ax = axes[i]
  1760. sub = results_master_df[results_master_df["Marker"] == marker]
  1761. if sub.empty:
  1762. ax.axis("off")
  1763. continue
  1764. rho_mat = sub.pivot(index="Structure", columns="Group", values="rho") \
  1765. .reindex(index=structure_order, columns=groups)
  1766. p_mat = sub.pivot(index="Structure", columns="Group", values="p_val") \
  1767. .reindex(index=structure_order, columns=groups)
  1768. pFDR_mat = sub.pivot(index="Structure", columns="Group", values="p_FDR") \
  1769. .reindex(index=structure_order, columns=groups)
  1770. annot = rho_mat.copy().astype(str)
  1771. for r in rho_mat.index:
  1772. for c in rho_mat.columns:
  1773. rv = rho_mat.loc[r, c]
  1774. pv = p_mat.loc[r, c]
  1775. pf = pFDR_mat.loc[r, c]
  1776. if not np.isnan(rv):
  1777. annot.loc[r, c] = f"{rv:.2f}{fdr_star(pf)}\n(p={pv:.3f})"
  1778. else:
  1779. annot.loc[r, c] = ""
  1780. sns.heatmap(
  1781. rho_mat, annot=annot, fmt="", cmap=cmap,
  1782. vmin=vmin, vmax=vmax, cbar=False,
  1783. linewidths=0.5, linecolor="white",
  1784. ax=ax, annot_kws={"fontsize": 9, "color": "black"}
  1785. )
  1786. ax.set_title(titles[marker], fontsize=13, color="black", pad=6)
  1787. ax.set_xticklabels(group_labels, fontsize=10, rotation=0, color="black")
  1788. ax.set_xlabel("")
  1789. ax.set_ylabel("")
  1790. if i % 3 == 0:
  1791. ax.set_yticklabels(
  1792. [s.capitalize().replace("_area", "") for s in structure_order],
  1793. fontsize=10, color="black"
  1794. )
  1795. else:
  1796. ax.set_yticklabels([])
  1797. # Turn off unused plots
  1798. for j in range(len(pathology_markers), 9):
  1799. axes[j].axis("off")
  1800. # ---------------------------------------------------------------------
  1801. # Figure title
  1802. # ---------------------------------------------------------------------
  1803. plt.suptitle(
  1804. "Partial Spearman correlation between postmortem MRI volumes\n"
  1805. "and global markers of pathology, degeneration and vascular burden",
  1806. fontsize=16, color="black", y=0.96
  1807. )
  1808. plt.tight_layout(rect=[0, 0, 1, 0.94])
  1809. plt.savefig("heatmap_addiotnal_markers.png", dpi=600, bbox_inches="tight")
  1810. plt.show()
  1811. ##########################################################################################
  1812. # Standalone colorbar (exact matching)
  1813. ##########################################################################################
  1814. fig, ax = plt.subplots(figsize=(1.2, 5))
  1815. sm = plt.cm.ScalarMappable(
  1816. cmap=cmap,
  1817. norm=plt.Normalize(vmin=vmin, vmax=vmax)
  1818. )
  1819. sm.set_array([])
  1820. cbar = plt.colorbar(sm, cax=ax)
  1821. cbar.set_label("Spearman ρ", fontsize=12, color="black", labelpad=6)
  1822. cbar.ax.tick_params(labelsize=10, colors="black", length=3)
  1823. for spine in ax.spines.values():
  1824. spine.set_visible(False)
  1825. plt.tight_layout()
  1826. plt.savefig("heatmap_addiotnal_markers_legend.png", dpi=600, bbox_inches="tight")
  1827. plt.show()
  1828. # %%
  1829. """
  1830. Generates standardized OLS β heatmaps for vascular pathology markers across four diagnostic groups and postmortem limbic/subcortical volumes.
  1831. For each group, marker, and structure, the script z-scores the ICV-normalized volume outcome, pathology marker, and covariates, fits covariate-adjusted OLS models, applies BH-FDR correction, and visualizes standardized β values with raw p-values and FDR-based significance stars.
  1832. """
  1833. # ---------------------------------------------------------------------
  1834. # GLOBAL SETTINGS
  1835. # ---------------------------------------------------------------------
  1836. matplotlib.rcParams["font.family"] = "DejaVu Sans"
  1837. warnings.filterwarnings("ignore", category=RuntimeWarning)
  1838. np.seterr(divide="ignore", invalid="ignore")
  1839. sns.set(style="white", context="talk")
  1840. # ---------------------------------------------------------------------
  1841. # USE df AS INPUT
  1842. # ---------------------------------------------------------------------
  1843. df_use = df.copy()
  1844. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  1845. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  1846. # ---------------------------------------------------------------------
  1847. # Groups (4)
  1848. # ---------------------------------------------------------------------
  1849. groups = [
  1850. "alzheimer's disease",
  1851. "lewy body disease",
  1852. "ftld-tdp",
  1853. "tauopathies"
  1854. ]
  1855. pretty_group = {
  1856. "alzheimer's disease": "AD",
  1857. "lewy body disease": "LBD",
  1858. "ftld-tdp": "FTLD-TDP",
  1859. "tauopathies": "FTLD-Tau"
  1860. }
  1861. group_labels = [pretty_group[g] for g in groups]
  1862. # ---------------------------------------------------------------------
  1863. # Structures + normalization
  1864. # ---------------------------------------------------------------------
  1865. structures = [
  1866. "hippocampus", "amygdala", "caudate",
  1867. "putamen", "thalamus", "pallidum", "accumbens_area"
  1868. ]
  1869. pretty_y = {
  1870. "hippocampus": "Hippocampus",
  1871. "amygdala": "Amygdala",
  1872. "caudate": "Caudate",
  1873. "putamen": "Putamen",
  1874. "thalamus": "Thalamus",
  1875. "pallidum": "Pallidum",
  1876. "accumbens_area": "Accumbens"
  1877. }
  1878. for s in structures:
  1879. pm = f"postmortem_{s}"
  1880. if pm in df_use.columns and "antemortem_icv" in df_use.columns:
  1881. df_use[f"{pm}_norm"] = df_use[pm] / df_use["antemortem_icv"]
  1882. # ---------------------------------------------------------------------
  1883. # Markers (3)
  1884. # ---------------------------------------------------------------------
  1885. pathology_markers = ["CAA", "Arteriolosclerosis", "Atherosclerosis"]
  1886. titles = {
  1887. "CAA": "CAA",
  1888. "Arteriolosclerosis": "Arteriolosclerosis",
  1889. "Atherosclerosis": "Atherosclerosis"
  1890. }
  1891. # ---------------------------------------------------------------------
  1892. # Convert vascular severity text to numeric
  1893. # ---------------------------------------------------------------------
  1894. severity_map = {
  1895. "0": 0, "None": 0, "None/Normal": 0,
  1896. "1": 1, "Mild": 1,
  1897. "2": 2, "Moderate": 2,
  1898. "3": 3, "Severe": 3
  1899. }
  1900. for m in pathology_markers:
  1901. if m in df_use.columns:
  1902. df_use[m] = (
  1903. df_use[m].astype(str)
  1904. .replace("Unknown", np.nan)
  1905. .replace(severity_map)
  1906. .replace("nan", np.nan)
  1907. )
  1908. df_use[m] = pd.to_numeric(df_use[m], errors="coerce")
  1909. # ---------------------------------------------------------------------
  1910. # Covariates
  1911. # ---------------------------------------------------------------------
  1912. covars = ["AgeatDeath", "Sex", "PMI", "Education"]
  1913. # ---------------------------------------------------------------------
  1914. # Helpers
  1915. # ---------------------------------------------------------------------
  1916. def fdr_star(p):
  1917. if pd.isna(p):
  1918. return ""
  1919. if p < 0.001:
  1920. return "***"
  1921. if p < 0.01:
  1922. return "**"
  1923. if p < 0.05:
  1924. return "*"
  1925. return ""
  1926. def zscore_inplace(df_in, cols):
  1927. out = df_in.copy()
  1928. for c in cols:
  1929. sd = out[c].std(ddof=0)
  1930. if (sd == 0) or np.isnan(sd):
  1931. out[c] = np.nan
  1932. else:
  1933. out[c] = (out[c] - out[c].mean()) / sd
  1934. return out
  1935. # ---------------------------------------------------------------------
  1936. # Compute OLS standardized β for each (marker, group, structure)
  1937. # Standardization happens WITHIN EACH GROUP for:
  1938. # y, marker, and covariates
  1939. # ---------------------------------------------------------------------
  1940. rows = []
  1941. MIN_N = 8 # can change if needed
  1942. for marker in pathology_markers:
  1943. for g in groups:
  1944. sub = df_use[df_use["NPDx1"] == g].copy()
  1945. if sub.empty:
  1946. for s in structures:
  1947. rows.append({
  1948. "Marker": marker,
  1949. "Group": g,
  1950. "Structure": s,
  1951. "beta_std": np.nan,
  1952. "p_raw": np.nan,
  1953. "N": 0
  1954. })
  1955. continue
  1956. for s in structures:
  1957. y = f"postmortem_{s}_norm"
  1958. if (marker not in sub.columns) or (y not in sub.columns):
  1959. rows.append({
  1960. "Marker": marker,
  1961. "Group": g,
  1962. "Structure": s,
  1963. "beta_std": np.nan,
  1964. "p_raw": np.nan,
  1965. "N": 0
  1966. })
  1967. continue
  1968. d = sub[[marker, y] + covars].apply(pd.to_numeric, errors="coerce").dropna()
  1969. if len(d) < MIN_N:
  1970. rows.append({
  1971. "Marker": marker,
  1972. "Group": g,
  1973. "Structure": s,
  1974. "beta_std": np.nan,
  1975. "p_raw": np.nan,
  1976. "N": int(len(d))
  1977. })
  1978. continue
  1979. # z-score within group
  1980. dz = zscore_inplace(d, [marker, y] + covars).dropna()
  1981. if len(dz) < MIN_N:
  1982. rows.append({
  1983. "Marker": marker,
  1984. "Group": g,
  1985. "Structure": s,
  1986. "beta_std": np.nan,
  1987. "p_raw": np.nan,
  1988. "N": int(len(dz))
  1989. })
  1990. continue
  1991. # OLS: z(y) ~ z(marker) + z(covariates)
  1992. X = sm.add_constant(dz[[marker] + covars], has_constant="add")
  1993. fit = sm.OLS(dz[y], X).fit()
  1994. rows.append({
  1995. "Marker": marker,
  1996. "Group": g,
  1997. "Structure": s,
  1998. "beta_std": float(fit.params.get(marker, np.nan)),
  1999. "p_raw": float(fit.pvalues.get(marker, np.nan)),
  2000. "N": int(len(dz))
  2001. })
  2002. ols_master = pd.DataFrame(rows)
  2003. # ---------------------------------------------------------------------
  2004. # FDR correction:
  2005. # For each marker, correct across ALL (structure × group) tests
  2006. # Stars come from p_FDR, while raw p is displayed
  2007. # ---------------------------------------------------------------------
  2008. ols_master["p_FDR"] = np.nan
  2009. for marker in pathology_markers:
  2010. idx = ols_master["Marker"] == marker
  2011. pvals = ols_master.loc[idx, "p_raw"].values
  2012. ok = ~np.isnan(pvals)
  2013. if ok.sum() > 0:
  2014. _, p_corr = pg.multicomp(pvals[ok], method="fdr_bh")
  2015. out = np.full_like(pvals, np.nan, dtype=float)
  2016. out[ok] = p_corr
  2017. ols_master.loc[idx, "p_FDR"] = out
  2018. # ---------------------------------------------------------------------
  2019. # Plot heatmaps (3 panels, one per marker) with 4 groups (columns)
  2020. # ---------------------------------------------------------------------
  2021. cmap = sns.diverging_palette(280, 150, s=90, l=60, as_cmap=True)
  2022. absmax = np.nanmax(np.abs(ols_master["beta_std"].values))
  2023. if not np.isfinite(absmax) or absmax == 0:
  2024. absmax = 0.5
  2025. vmin, vmax = -absmax, absmax
  2026. fig, axes = plt.subplots(1, 3, figsize=(24, 10))
  2027. axes = np.array(axes).flatten()
  2028. for i, marker in enumerate(pathology_markers):
  2029. ax = axes[i]
  2030. sub = ols_master[ols_master["Marker"] == marker].copy()
  2031. beta_mat = (
  2032. sub.pivot(index="Structure", columns="Group", values="beta_std")
  2033. .reindex(index=structures, columns=groups)
  2034. )
  2035. p_mat = (
  2036. sub.pivot(index="Structure", columns="Group", values="p_raw")
  2037. .reindex(index=structures, columns=groups)
  2038. )
  2039. pf_mat = (
  2040. sub.pivot(index="Structure", columns="Group", values="p_FDR")
  2041. .reindex(index=structures, columns=groups)
  2042. )
  2043. annot = beta_mat.copy().astype(object)
  2044. for r in beta_mat.index:
  2045. for c in beta_mat.columns:
  2046. b = beta_mat.loc[r, c]
  2047. pr = p_mat.loc[r, c]
  2048. pf = pf_mat.loc[r, c]
  2049. if pd.notna(b) and pd.notna(pr):
  2050. annot.loc[r, c] = f"β={b:.2f}{fdr_star(pf)}\n(p={pr:.3f})"
  2051. else:
  2052. annot.loc[r, c] = ""
  2053. sns.heatmap(
  2054. beta_mat,
  2055. annot=annot,
  2056. fmt="",
  2057. cmap=cmap,
  2058. vmin=vmin,
  2059. vmax=vmax,
  2060. cbar=(i == 2),
  2061. linewidths=0.6,
  2062. linecolor="white",
  2063. ax=ax,
  2064. annot_kws={"fontsize": 22, "color": "black"}
  2065. )
  2066. ax.set_title(titles[marker], fontsize=22, pad=10, color="black")
  2067. ax.set_xticklabels(group_labels, fontsize=16, rotation=0, color="black")
  2068. ax.set_xlabel("")
  2069. ax.set_ylabel("")
  2070. if i == 0:
  2071. ax.set_yticklabels([pretty_y[s] for s in structures], fontsize=18, color="black")
  2072. else:
  2073. ax.set_yticklabels([])
  2074. # ---------------------------------------------------------------------
  2075. # Format colorbar
  2076. # ---------------------------------------------------------------------
  2077. cbar = axes[-1].collections[0].colorbar
  2078. cbar.set_label("Standardized β (marker effect)", fontsize=18, labelpad=10)
  2079. cbar.ax.tick_params(labelsize=16)
  2080. plt.tight_layout(rect=[0, 0, 1, 0.95])
  2081. plt.savefig("heatmap_beta_rawp_FDRstars_vascular_4groups.jpg", dpi=600, bbox_inches="tight")
  2082. plt.show()
  2083. # %%
  2084. ##########################################################################################
  2085. ########## Linear Mixed Effects Model: Polypathology
  2086. ##########################################################################################
  2087. # %%
  2088. """
  2089. Generates 2×2 disease-specific panels showing standardized pathology–volume associations across postmortem limbic/subcortical structures.
  2090. For each diagnostic group and structure, the script fits covariate-adjusted standardized OLS models using Tau, α-synuclein, and TDP-43 pathology predictors, applies BH-FDR correction within each disease group, and visualizes standardized β coefficients as grouped bar plots with significance annotations.
  2091. """
  2092. # ---------------------------------------------------------------------
  2093. # Use your cleaned dataframe exactly as-is
  2094. # ---------------------------------------------------------------------
  2095. df_use = df.copy() # ← very important
  2096. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  2097. df_use["Sex"] = pd.to_numeric(df_use["Sex"], errors="coerce")
  2098. disease_groups = [
  2099. "alzheimer's disease",
  2100. "lewy body disease",
  2101. "ftld-tdp",
  2102. "tauopathies"
  2103. ]
  2104. df_use = df_use[df_use["NPDx1"].isin(disease_groups)].copy()
  2105. # ---------------------------------------------------------------------
  2106. # Structures + region prefixes
  2107. # ---------------------------------------------------------------------
  2108. structures = ["hippocampus", "amygdala", "caudate", "putamen", "thalamus", "pallidum"]
  2109. region_map = {
  2110. "hippocampus": "EC_CS_DG",
  2111. "amygdala": "Amyg",
  2112. "caudate": "CP",
  2113. "putamen": "CP",
  2114. "thalamus": "TS",
  2115. "pallidum": "GP",
  2116. }
  2117. # Normalize volumes
  2118. if "antemortem_icv" in df_use.columns:
  2119. for s in structures:
  2120. pm = f"postmortem_{s}"
  2121. if pm in df_use.columns:
  2122. df_use[f"{pm}_norm"] = df_use[pm] / df_use["antemortem_icv"]
  2123. # Display names
  2124. pretty_group = {
  2125. "alzheimer's disease": "Alzheimer’s Disease",
  2126. "lewy body disease": "Lewy Body Disease",
  2127. "ftld-tdp": "FTLD-TDP",
  2128. "tauopathies": "Tauopathies"
  2129. }
  2130. # Colors per structure
  2131. structure_palette = {
  2132. "hippocampus": "#6a51a3",
  2133. "amygdala": "#9e9ac8",
  2134. "caudate": "#807dba",
  2135. "putamen": "#bcbddc",
  2136. "thalamus": "#cbc9e2",
  2137. "pallidum": "#dadaeb"
  2138. }
  2139. # ---------------------------------------------------------------------
  2140. # Fit LME for a single structure
  2141. # ---------------------------------------------------------------------
  2142. def fit_structure(df_sub, s):
  2143. prefix = region_map[s]
  2144. # outcome (normalized preferred)
  2145. candidates = [f"postmortem_{s}_norm", f"postmortem_{s}"]
  2146. outcome = next((x for x in candidates if x in df_sub.columns), None)
  2147. if outcome is None:
  2148. return pd.DataFrame()
  2149. # pathology predictors: *only Tau, aSyn, TDP43*
  2150. path_cols = [
  2151. c for c in df_sub.columns
  2152. if c.startswith(prefix)
  2153. and any(x in c for x in ["Tau", "aSyn", "TDP43"])
  2154. ]
  2155. if not path_cols:
  2156. return pd.DataFrame()
  2157. covars = [c for c in ["AgeatDeath", "Sex", "Education", "PMI"] if c in df_sub.columns]
  2158. keep = ["INDDID", outcome] + path_cols + covars
  2159. d = df_sub[keep].dropna()
  2160. if len(d) < 10:
  2161. return pd.DataFrame()
  2162. # Standardize numeric columns
  2163. for col in d.select_dtypes(include=[np.number]).columns:
  2164. if d[col].std(ddof=0) > 0:
  2165. d[col] = (d[col] - d[col].mean()) / d[col].std(ddof=0)
  2166. formula = outcome + " ~ " + " + ".join(path_cols + covars)
  2167. try:
  2168. model = smf.ols(formula, d).fit()
  2169. except:
  2170. return pd.DataFrame()
  2171. out = []
  2172. for param, coef, pval in zip(model.params.index, model.params.values, model.pvalues.values):
  2173. if param == "Intercept":
  2174. continue
  2175. if any(x in param for x in ["Tau", "aSyn", "TDP43"]):
  2176. out.append({
  2177. "Structure": s,
  2178. "Marker": param.replace(prefix, ""),
  2179. "StdBeta": coef,
  2180. "p": pval,
  2181. "N": len(d)
  2182. })
  2183. return pd.DataFrame(out)
  2184. # ---------------------------------------------------------------------
  2185. # 2×2 PANEL FIGURE (one panel per disease)
  2186. # ---------------------------------------------------------------------
  2187. fig, axes = plt.subplots(2, 2, figsize=(16, 12), sharey=True)
  2188. axes = axes.flatten()
  2189. for idx, dx in enumerate(disease_groups):
  2190. ax = axes[idx]
  2191. gdf = df_use[df_use["NPDx1"] == dx].copy()
  2192. if gdf.empty:
  2193. ax.text(0.5, 0.5, "No data", ha="center", va="center")
  2194. ax.axis("off")
  2195. continue
  2196. all_out = []
  2197. for s in structures:
  2198. r = fit_structure(gdf, s)
  2199. if not r.empty:
  2200. all_out.append(r)
  2201. if not all_out:
  2202. ax.text(0.5, 0.5, "No valid models", ha="center", va="center")
  2203. ax.axis("off")
  2204. continue
  2205. df_dx = pd.concat(all_out, ignore_index=True)
  2206. # FDR per disease
  2207. _, p_corr = pg.multicomp(df_dx["p"], method="fdr_bh")
  2208. df_dx["p_FDR"] = p_corr
  2209. df_dx["Sig"] = df_dx["p_FDR"].apply(
  2210. lambda p: "***" if p < 0.001 else "**"
  2211. if p < 0.01 else "*" if p < 0.05 else ""
  2212. )
  2213. # Short clean marker names
  2214. df_dx["MarkerClean"] = df_dx["Marker"].replace({
  2215. "Tau": "Tau",
  2216. "aSyn": "aSyn",
  2217. "TDP43": "TDP43"
  2218. })
  2219. # Barplot
  2220. sns.barplot(
  2221. data=df_dx,
  2222. x="MarkerClean", y="StdBeta",
  2223. hue="Structure",
  2224. palette=structure_palette,
  2225. errorbar=None, ax=ax
  2226. )
  2227. # Add significance stars
  2228. for _, r in df_dx.iterrows():
  2229. x = ["Tau", "aSyn", "TDP43"].index(r["MarkerClean"])
  2230. y = r["StdBeta"]
  2231. ax.text(
  2232. x + (structures.index(r["Structure"]) * 0.015),
  2233. y + np.sign(y) * 0.03,
  2234. r["Sig"],
  2235. ha="center", fontsize=11, weight="bold"
  2236. )
  2237. ax.axhline(0, color="black", lw=1)
  2238. ax.set_title(pretty_group[dx], fontsize=15, weight="bold")
  2239. ax.set_xlabel("")
  2240. if idx % 2 == 0:
  2241. ax.set_ylabel("Std β")
  2242. else:
  2243. ax.set_ylabel("")
  2244. # Legend
  2245. handles, labels = axes[0].get_legend_handles_labels()
  2246. fig.legend(
  2247. handles, labels, title="Structure",
  2248. bbox_to_anchor=(0.5, -0.02), loc="lower center",
  2249. ncol=6, frameon=False
  2250. )
  2251. plt.suptitle("Postmortem Polypathology–Volume Effects (LME, standardized β)", fontsize=17)
  2252. plt.tight_layout(rect=[0.05, 0.05, 1, 0.95])
  2253. plt.show()
  2254. # %%
  2255. # %%
  2256. """
  2257. Runs disease- and structure-specific standardized postmortem pathology–volume models for limbic and subcortical regions.
  2258. For each diagnostic group and structure, the script fits covariate-adjusted standardized OLS models using Tau, TDP-43, and α-synuclein pathology predictors, extracts β coefficients, standard errors, 95% confidence intervals, raw and FDR-corrected p-values, and visualizes pathology-specific standardized effects in 2×2 disease panels.
  2259. """
  2260. import pandas as pd
  2261. import numpy as np
  2262. import statsmodels.formula.api as smf
  2263. import seaborn as sns
  2264. import matplotlib.pyplot as plt
  2265. import pingouin as pg
  2266. import warnings, matplotlib
  2267. warnings.filterwarnings("ignore")
  2268. matplotlib.rcParams['font.family'] = 'DejaVu Sans'
  2269. # ===============================================================
  2270. # 🔧 1. Load YOUR dataset
  2271. # ===============================================================
  2272. df_use = df.copy()
  2273. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  2274. if "Sex" in df_use.columns:
  2275. df_use["Sex"] = pd.to_numeric(df_use["Sex"], errors="coerce")
  2276. groups = ["alzheimer's disease", "lewy body disease", "ftld-tdp", "tauopathies"]
  2277. df_use = df_use[df_use["NPDx1"].isin(groups)].copy()
  2278. # ===============================================================
  2279. # 🔧 2. Structures + pathology prefixes
  2280. # ===============================================================
  2281. structures = ["hippocampus", "amygdala", "caudate", "putamen", "thalamus", "pallidum"]
  2282. region_map = {
  2283. "hippocampus": "EC_CS_DG",
  2284. "amygdala": "Amyg",
  2285. "caudate": "CP",
  2286. "putamen": "CP",
  2287. "thalamus": "TS",
  2288. "pallidum": "GP",
  2289. }
  2290. # Normalize volumes by ICV
  2291. if "antemortem_icv" in df_use.columns:
  2292. for s in structures:
  2293. pm = f"postmortem_{s}"
  2294. if pm in df_use.columns:
  2295. df_use[f"{pm}_norm"] = df_use[pm] / df_use["antemortem_icv"]
  2296. # ===============================================================
  2297. # 🔧 3. Run LME for each disease group × structure
  2298. # ===============================================================
  2299. def run_lme_by_group(df_sub, group_name):
  2300. gdf = df_sub[df_sub["NPDx1"] == group_name].copy()
  2301. results = []
  2302. if gdf.empty:
  2303. print(f"[skip] {group_name}: no subjects")
  2304. return pd.DataFrame()
  2305. for s in structures:
  2306. # pick normalized → raw
  2307. out_candidates = [f"postmortem_{s}_norm", f"postmortem_{s}"]
  2308. outcome = next((x for x in out_candidates if x in gdf.columns), None)
  2309. if outcome is None:
  2310. continue
  2311. prefix = region_map[s]
  2312. # pathology predictors: Tau / aSyn / TDP43
  2313. path_cols = [
  2314. c for c in gdf.columns
  2315. if c.startswith(prefix) and any(k in c for k in ["Tau", "aSyn", "TDP43"])
  2316. ]
  2317. if not path_cols:
  2318. print(f"[skip] {group_name} {s}: no matching pathology variables ({prefix})")
  2319. continue
  2320. covars = [c for c in ["AgeatDeath", "Sex", "Education", "PMI"] if c in gdf.columns]
  2321. keep = ["INDDID", outcome] + path_cols + covars
  2322. d = gdf[keep].dropna()
  2323. if len(d) < 10:
  2324. print(f"[skip] {group_name} {s}: insufficient N={len(d)}")
  2325. continue
  2326. # --------------------
  2327. # Standardize
  2328. # --------------------
  2329. for col in d.select_dtypes(include=[np.number]).columns:
  2330. if d[col].std(ddof=0) > 0:
  2331. d[col] = (d[col] - d[col].mean()) / d[col].std(ddof=0)
  2332. rhs = path_cols + covars
  2333. formula = f"{outcome} ~ " + " + ".join(rhs)
  2334. print("\n--------------------------------------------------------")
  2335. print(f"GROUP: {group_name.upper()} | STRUCTURE: {s.upper()}")
  2336. print("MODEL FORMULA:")
  2337. print(formula)
  2338. print("--------------------------------------------------------")
  2339. try:
  2340. model = smf.ols(formula, d).fit()
  2341. except Exception as e:
  2342. print("ERROR:", e)
  2343. continue
  2344. """
  2345. for p, coef, pval in zip(model.params.index, model.params.values, model.pvalues.values):
  2346. if p == "Intercept":
  2347. continue
  2348. results.append({
  2349. "Group": group_name,
  2350. "Structure": s,
  2351. "Parameter": p,
  2352. "StdBeta": coef,
  2353. "p-value": pval,
  2354. "N": len(d)
  2355. })
  2356. """
  2357. # inside the loop: for p, coef, pval in zip(...)
  2358. for p, coef, pval in zip(model.params.index, model.params.values, model.pvalues.values):
  2359. if p == "Intercept":
  2360. continue
  2361. se = model.bse.get(p, np.nan)
  2362. ci_low = coef - 1.96 * se
  2363. ci_high = coef + 1.96 * se
  2364. results.append({
  2365. "Group": group_name,
  2366. "Structure": s,
  2367. "Parameter": p,
  2368. "StdBeta": coef,
  2369. "SE": se,
  2370. "CI_low": ci_low,
  2371. "CI_high": ci_high,
  2372. "p-value": pval,
  2373. "N": len(d)
  2374. })
  2375. res_df = pd.DataFrame(results)
  2376. if not res_df.empty:
  2377. _, p_corr = pg.multicomp(res_df["p-value"], method="fdr_bh")
  2378. res_df["p-FDR"] = p_corr
  2379. res_df["Sig(FDR)"] = res_df["p-FDR"].apply(
  2380. lambda p: "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
  2381. )
  2382. return res_df
  2383. # ===============================================================
  2384. # 🔧 4. Run all models
  2385. # ===============================================================
  2386. all_results = []
  2387. for g in groups:
  2388. print(f"\n\n====================== {g.upper()} ======================")
  2389. out = run_lme_by_group(df_use, g)
  2390. if not out.empty:
  2391. all_results.append(out)
  2392. print(out.to_string(index=False))
  2393. else:
  2394. print("NO MODELS FIT.")
  2395. # ===============================================================
  2396. # 🔧 5. Combine results
  2397. # ===============================================================
  2398. if not all_results:
  2399. raise SystemExit("❌ No results to plot")
  2400. results_df = pd.concat(all_results, ignore_index=True)
  2401. # only pathology rows
  2402. subset = results_df[
  2403. results_df["Parameter"].str.contains("Tau|TDP43|aSyn", case=False)
  2404. ]
  2405. # ===============================================================
  2406. # 🔧 6. Plot — 2×2 GRID (one panel per disease)
  2407. # ===============================================================
  2408. sns.set(style="whitegrid", context="talk")
  2409. fig, axes = plt.subplots(2, 2, figsize=(12, 8), sharey=True)
  2410. axes = axes.flatten()
  2411. for i, g in enumerate(groups):
  2412. ax = axes[i]
  2413. sub = subset[subset["Group"] == g]
  2414. if sub.empty:
  2415. ax.text(0.5, 0.5, "No data", ha="center", va="center")
  2416. ax.axis("off")
  2417. continue
  2418. sns.barplot(
  2419. data=sub,
  2420. x="Parameter", y="StdBeta",
  2421. hue="Structure",
  2422. errorbar=None, ax=ax
  2423. )
  2424. ax.axhline(0, color="black", lw=1)
  2425. ax.tick_params(axis="x", rotation=90, labelsize=8)
  2426. ax.set_title(g.title(), fontsize=13)
  2427. ax.set_xlabel("")
  2428. ax.set_ylabel("Std β")
  2429. handles, labels = axes[0].get_legend_handles_labels()
  2430. fig.legend(handles, labels, title="Structure",
  2431. loc="lower center", bbox_to_anchor=(0.5, -0.05),
  2432. ncol=6, frameon=False)
  2433. plt.suptitle("Postmortem Polypathology–Volume Associations (Standardized β)", fontsize=16)
  2434. plt.tight_layout(rect=[0,0.05,1,0.95])
  2435. plt.show()
  2436. # %%
  2437. """
  2438. Exports postmortem polypathology model results to a publication-ready Word summary table.
  2439. The script formats disease-specific tables reporting structure, predictor, standardized β, SE, 95% CI, raw p-values, and FDR-corrected p-values with superscript significance markers, then saves the results as a DOCX file.
  2440. """
  2441. import pandas as pd
  2442. from docx import Document
  2443. from docx.enum.table import WD_TABLE_ALIGNMENT
  2444. from docx.oxml import OxmlElement
  2445. from docx.oxml.ns import qn
  2446. df_in = results_df.copy()
  2447. # ======================================================================================
  2448. # Pretty names
  2449. # ======================================================================================
  2450. pretty_group = {
  2451. "alzheimer's disease": "Alzheimer’s disease",
  2452. "lewy body disease": "Lewy body disease",
  2453. "ftld-tdp": "FTLD-TDP",
  2454. "tauopathies": "Tauopathies",
  2455. }
  2456. pretty_struct = {
  2457. "hippocampus": "Hippocampus",
  2458. "amygdala": "Amygdala",
  2459. "caudate": "Caudate",
  2460. "putamen": "Putamen",
  2461. "thalamus": "Thalamus",
  2462. "pallidum": "Pallidum",
  2463. }
  2464. pretty_param = {
  2465. "AgeatDeath": "Age at death",
  2466. "Sex": "Sex",
  2467. "Education": "Education",
  2468. "PMI": "PMI",
  2469. }
  2470. def clean_param(p):
  2471. if "Tau" in p: return "p-tau"
  2472. if "aSyn" in p: return "α-synuclein"
  2473. if "TDP43" in p: return "TDP-43"
  2474. return pretty_param.get(p, p)
  2475. # ======================================================================================
  2476. # Superscript helper
  2477. # ======================================================================================
  2478. def add_superscript(run, text):
  2479. for ch in text:
  2480. r = OxmlElement('w:r')
  2481. t = OxmlElement('w:t')
  2482. t.text = ch
  2483. r.append(t)
  2484. rpr = OxmlElement('w:rPr')
  2485. vert = OxmlElement('w:vertAlign')
  2486. vert.set(qn('w:val'), 'superscript')
  2487. rpr.append(vert)
  2488. r.append(rpr)
  2489. run._r.addnext(r)
  2490. # ======================================================================================
  2491. # DOCX init
  2492. # ======================================================================================
  2493. doc = Document()
  2494. title = doc.add_heading("Polypathology LME – Postmortem MRI", level=1)
  2495. title.alignment = 1
  2496. cap = (
  2497. "Linear regression models predicting postmortem MRI volumes from multiple pathology "
  2498. "burdens (p-tau, α-synuclein, TDP-43) and covariates (age at death, sex, education, PMI). "
  2499. "Reported: standardized β, standard error (SE), 95% confidence interval (CI), raw p-value "
  2500. "and FDR-corrected p-value with superscript significance (*, **, ***). "
  2501. "One table per disease group."
  2502. )
  2503. doc.add_paragraph(cap)
  2504. doc.add_page_break()
  2505. # ======================================================================================
  2506. # Build one table per disease group
  2507. # ======================================================================================
  2508. for g in pretty_group.keys():
  2509. sub = df_in[df_in["Group"] == g].copy()
  2510. if sub.empty:
  2511. continue
  2512. doc.add_heading(pretty_group[g], level=2)
  2513. sub["ParamPrint"] = sub["Parameter"].apply(clean_param)
  2514. sub.sort_values(["Structure", "ParamPrint"], inplace=True)
  2515. structs = sub["Structure"].unique()
  2516. total_rows = sum(len(sub[sub["Structure"] == s]) for s in structs)
  2517. # ⭐ NOW 7 columns instead of 6
  2518. table = doc.add_table(rows=total_rows + 1, cols=7)
  2519. table.style = "Table Grid"
  2520. table.alignment = WD_TABLE_ALIGNMENT.CENTER
  2521. # Header
  2522. hdr = table.rows[0].cells
  2523. hdr[0].text = "Structure"
  2524. hdr[1].text = "Predictor"
  2525. hdr[2].text = "Std β"
  2526. hdr[3].text = "SE"
  2527. hdr[4].text = "CI (95%)"
  2528. hdr[5].text = "p(raw)"
  2529. hdr[6].text = "p(FDR)"
  2530. row_idx = 1
  2531. for s in structs:
  2532. block = sub[sub["Structure"] == s]
  2533. span = len(block)
  2534. base = table.cell(row_idx, 0)
  2535. for _ in range(span - 1):
  2536. base.merge(table.cell(row_idx + 1, 0))
  2537. base.text = pretty_struct[s]
  2538. for _, r in block.iterrows():
  2539. cells = table.rows[row_idx].cells
  2540. cells[1].text = r["ParamPrint"]
  2541. cells[2].text = f"{r['StdBeta']:.3f}"
  2542. cells[3].text = f"{r['SE']:.3f}"
  2543. cells[4].text = f"[{r['CI_low']:.3f}, {r['CI_high']:.3f}]"
  2544. # p(raw)
  2545. cells[5].text = f"{r['p-value']:.2e}"
  2546. # p(FDR) with superscripts
  2547. run = cells[6].paragraphs[0].add_run(f"{r['p-FDR']:.2e}")
  2548. if r["Sig(FDR)"]:
  2549. add_superscript(run, r["Sig(FDR)"])
  2550. row_idx += 1
  2551. doc.add_page_break()
  2552. # ======================================================================================
  2553. # SAVE
  2554. # ======================================================================================
  2555. doc.save("polypathology_LME_results.docx")
  2556. print("Saved: polypathology_LME_results.docx")
  2557. # %%
  2558. # %%
  2559. ##########################################################################################
  2560. # Polypathology LME Heatmaps (FINAL VERSION)
  2561. # Updated with:
  2562. # - β + raw p in cell
  2563. # - FDR stars (***) based on p-FDR
  2564. # - "Not applicable" explicitly shown for missing cells
  2565. # - Missing cells are masked so they do NOT get misleading colors
  2566. ##########################################################################################
  2567. import seaborn as sns
  2568. import matplotlib.pyplot as plt
  2569. import numpy as np
  2570. import pandas as pd
  2571. import matplotlib
  2572. from matplotlib import colors
  2573. matplotlib.rcParams["font.family"] = "DejaVu Sans"
  2574. sns.set(style="white", context="talk")
  2575. def plot_polypathology_heatmaps_dfuse(
  2576. results_df,
  2577. groups=(
  2578. "alzheimer's disease",
  2579. "lewy body disease",
  2580. "ftld-tdp",
  2581. "tauopathies"
  2582. ),
  2583. out_png="lme_polypathology_heatmaps_clean_final.png"
  2584. ):
  2585. # ----------------------------------------------------------
  2586. # 1) Ensure Group column exists
  2587. # ----------------------------------------------------------
  2588. if "Group" not in results_df.columns:
  2589. if "NPDx1" in results_df.columns:
  2590. results_df = results_df.rename(columns={"NPDx1": "Group"})
  2591. else:
  2592. raise ValueError("results_df must contain Group or NPDx1 column.")
  2593. # ----------------------------------------------------------
  2594. # 2) Filter predictors (Tau, TDP43, aSyn)
  2595. # ----------------------------------------------------------
  2596. mask = results_df["Parameter"].str.contains("Tau|TDP43|aSyn", case=False, na=False)
  2597. gdf = results_df[mask].copy()
  2598. def to_pred(p):
  2599. p = str(p)
  2600. if "TDP43" in p:
  2601. return "TDP-43"
  2602. if "Tau" in p:
  2603. return "p-tau"
  2604. if "aSyn" in p:
  2605. return "α-synuclein"
  2606. return None
  2607. gdf["Predictor"] = gdf["Parameter"].apply(to_pred)
  2608. gdf = gdf[~gdf["Predictor"].isna()].copy()
  2609. # ----------------------------------------------------------
  2610. # 3) Structure ordering
  2611. # ----------------------------------------------------------
  2612. struct_order = ["hippocampus", "amygdala", "caudate",
  2613. "putamen", "thalamus", "pallidum"]
  2614. struct_label_map = {s: s.capitalize() for s in struct_order}
  2615. gdf = gdf[gdf["Structure"].isin(struct_order)].copy()
  2616. gdf["StructureLabel"] = gdf["Structure"].map(struct_label_map)
  2617. predictor_order = ["TDP-43", "p-tau", "α-synuclein"]
  2618. # ----------------------------------------------------------
  2619. # 4) Color map (soft PRGn)
  2620. # ----------------------------------------------------------
  2621. base_cmap = sns.color_palette("PRGn", as_cmap=True)
  2622. cmap = colors.LinearSegmentedColormap.from_list(
  2623. "PRGn_soft",
  2624. base_cmap(np.linspace(0.1, 0.9, 256))
  2625. )
  2626. # ----------------------------------------------------------
  2627. # 5) Pretty titles
  2628. # ----------------------------------------------------------
  2629. title_map = {
  2630. "alzheimer's disease": "Alzheimer’s disease",
  2631. "lewy body disease": "Lewy body disease",
  2632. "ftld-tdp": "FTLD-TDP",
  2633. "tauopathies": "FTLD-Tau"
  2634. }
  2635. # ----------------------------------------------------------
  2636. # 6) FDR star helper
  2637. # ----------------------------------------------------------
  2638. def fdr_star(pf):
  2639. if pd.isna(pf):
  2640. return ""
  2641. if pf < 0.001:
  2642. return "***"
  2643. if pf < 0.01:
  2644. return "**"
  2645. if pf < 0.05:
  2646. return "*"
  2647. return ""
  2648. # ----------------------------------------------------------
  2649. # 7) Create figure (2×2)
  2650. # ----------------------------------------------------------
  2651. fig, axes = plt.subplots(2, 2, figsize=(12.5, 9))
  2652. axes = axes.flatten()
  2653. for i, g in enumerate(groups):
  2654. ax = axes[i]
  2655. sub = gdf[gdf["Group"] == g].copy()
  2656. if sub.empty:
  2657. ax.text(0.5, 0.5, "No data", ha="center", va="center")
  2658. ax.axis("off")
  2659. continue
  2660. # ----------------------------------------------------------
  2661. # Pivot tables
  2662. # ----------------------------------------------------------
  2663. beta_pivot = sub.pivot_table(
  2664. index="StructureLabel", columns="Predictor", values="StdBeta"
  2665. ).reindex(
  2666. index=[struct_label_map[s] for s in struct_order],
  2667. columns=predictor_order
  2668. )
  2669. pval_pivot = sub.pivot_table(
  2670. index="StructureLabel", columns="Predictor", values="p-value"
  2671. ).reindex(index=beta_pivot.index, columns=beta_pivot.columns)
  2672. pfdr_pivot = sub.pivot_table(
  2673. index="StructureLabel", columns="Predictor", values="p-FDR"
  2674. ).reindex(index=beta_pivot.index, columns=beta_pivot.columns)
  2675. # ----------------------------------------------------------
  2676. # Mask missing β (these cells become "Not applicable")
  2677. # ----------------------------------------------------------
  2678. na_mask = beta_pivot.isna()
  2679. # ----------------------------------------------------------
  2680. # Heatmap background (masked so NA cells are blank/white)
  2681. # ----------------------------------------------------------
  2682. sns.heatmap(
  2683. beta_pivot,
  2684. ax=ax,
  2685. cmap=cmap,
  2686. center=0,
  2687. vmin=-1, vmax=1,
  2688. mask=na_mask,
  2689. linewidths=0.6,
  2690. linecolor="white",
  2691. cbar=False,
  2692. annot=False
  2693. )
  2694. # ----------------------------------------------------------
  2695. # Axis formatting
  2696. # ----------------------------------------------------------
  2697. ax.set_yticks(np.arange(len(struct_order)) + 0.5)
  2698. ax.set_yticklabels([struct_label_map[s] for s in struct_order], fontsize=14)
  2699. # x-axis only on bottom row panels
  2700. if i in [2, 3]:
  2701. ax.set_xticks(np.arange(len(predictor_order)) + 0.5)
  2702. ax.set_xticklabels(predictor_order, fontsize=14)
  2703. else:
  2704. ax.set_xticks([])
  2705. ax.set_xticklabels([])
  2706. ax.set_xlabel("")
  2707. ax.set_ylabel("")
  2708. ax.tick_params(axis="both", length=0)
  2709. # ----------------------------------------------------------
  2710. # Annotate text: valid -> β + raw p + FDR stars
  2711. # missing -> "Not applicable"
  2712. # ----------------------------------------------------------
  2713. for y in range(beta_pivot.shape[0]):
  2714. for x in range(beta_pivot.shape[1]):
  2715. beta = beta_pivot.iloc[y, x]
  2716. pval = pval_pivot.iloc[y, x]
  2717. pfdr = pfdr_pivot.iloc[y, x]
  2718. # Not applicable if no beta OR no p-value
  2719. if pd.isna(beta) or pd.isna(pval) or np.isclose(beta, 0, atol=1e-6):
  2720. ax.text(
  2721. x + 0.5, y + 0.5,
  2722. "Not\napplicable",
  2723. ha="center", va="center",
  2724. fontsize=13,
  2725. color="black"
  2726. )
  2727. continue
  2728. star = fdr_star(pfdr)
  2729. text = f"β={beta:.2f}{star}\n(p={pval:.3f})"
  2730. ax.text(
  2731. x + 0.5, y + 0.5,
  2732. text,
  2733. ha="center", va="center",
  2734. fontsize=15,
  2735. color="black"
  2736. )
  2737. ax.set_title(title_map.get(g, g), fontsize=15, pad=12)
  2738. # Turn off any unused axes (if fewer than 4 groups)
  2739. for j in range(len(groups), 4):
  2740. axes[j].axis("off")
  2741. # ----------------------------------------------------------
  2742. # Global labels
  2743. # ----------------------------------------------------------
  2744. fig.text(0.04, 0.5, "Structure", va="center", rotation=90, fontsize=14)
  2745. fig.text(0.52, 0.06, "Predictor", ha="center", fontsize=14)
  2746. # ----------------------------------------------------------
  2747. # Shared colorbar
  2748. # ----------------------------------------------------------
  2749. cbar_ax = fig.add_axes([0.93, 0.28, 0.02, 0.45])
  2750. sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=-1, vmax=1))
  2751. sm.set_array([])
  2752. cbar = plt.colorbar(sm, cax=cbar_ax)
  2753. cbar.set_label("Std β", fontsize=12)
  2754. plt.tight_layout(rect=[0.08, 0.10, 0.9, 0.95])
  2755. plt.savefig(out_png, dpi=600, bbox_inches="tight")
  2756. plt.show()
  2757. # ---- RUN ----
  2758. plot_polypathology_heatmaps_dfuse(results_df)
  2759. # %%
  2760. ##########################################################################################
  2761. ########## Linear Mixed Effects Model: Primary pathology
  2762. ##########################################################################################
  2763. # %%
  2764. """
  2765. Fits standardized primary-pathology models linking disease-specific pathology burden to postmortem limbic/subcortical volumes while adjusting for key covariates.
  2766. The script runs one model set per diagnostic group, extracts standardized β estimates with SE, 95% CI, raw and FDR-corrected p-values, and exports the results as publication-ready DOCX tables.
  2767. """
  2768. ##########################################################################################
  2769. # 1) Load df_use
  2770. ##########################################################################################
  2771. df_use = df.copy()
  2772. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  2773. df_use["Sex"] = pd.to_numeric(df_use["Sex"], errors="coerce")
  2774. disease_groups = [
  2775. "alzheimer's disease",
  2776. "lewy body disease",
  2777. "ftld-tdp",
  2778. "tauopathies"
  2779. ]
  2780. df_use = df_use[df_use["NPDx1"].isin(disease_groups)].copy()
  2781. ##########################################################################################
  2782. # 2) Structures + prefixes
  2783. ##########################################################################################
  2784. structures = ["hippocampus", "amygdala", "caudate", "putamen",
  2785. "thalamus", "pallidum"]
  2786. region_prefix_map = {
  2787. "hippocampus": "EC_CS_DG",
  2788. "amygdala": "Amyg",
  2789. "caudate": "CP",
  2790. "putamen": "CP",
  2791. "thalamus": "TS",
  2792. "pallidum": "GP",
  2793. }
  2794. primary_marker_suffix = {
  2795. "alzheimer's disease": "Tau",
  2796. "lewy body disease": "aSyn",
  2797. "ftld-tdp": "TDP43",
  2798. "tauopathies": "Tau",
  2799. }
  2800. covars = ["AgeatDeath", "Sex", "Education", "PMI"]
  2801. ##########################################################################################
  2802. # 3) Normalize volumes
  2803. ##########################################################################################
  2804. if "antemortem_icv" in df_use.columns:
  2805. for s in structures:
  2806. pm = f"postmortem_{s}"
  2807. if pm in df_use.columns:
  2808. df_use[f"{pm}_norm"] = df_use[pm] / df_use["antemortem_icv"]
  2809. ##########################################################################################
  2810. # 4) LME WITH CI
  2811. ##########################################################################################
  2812. def run_primary_lme_by_group(df_use, group_name, structures):
  2813. results_all = []
  2814. marker_suffix = primary_marker_suffix[group_name]
  2815. gdf = df_use[df_use["NPDx1"] == group_name].copy()
  2816. if gdf.empty:
  2817. return pd.DataFrame()
  2818. for s in structures:
  2819. region_prefix = region_prefix_map[s]
  2820. # find pathology for this structure
  2821. path_candidates = [c for c in gdf.columns if c.lower().startswith(region_prefix.lower())]
  2822. path_cols = [c for c in path_candidates if marker_suffix.lower() in c.lower()]
  2823. if not path_cols:
  2824. continue
  2825. path_col = path_cols[0]
  2826. outcome = next(
  2827. (c for c in (f"postmortem_{s}_norm", f"postmortem_{s}")
  2828. if c in gdf.columns),
  2829. None
  2830. )
  2831. if outcome is None:
  2832. continue
  2833. covars_here = [c for c in covars if c in gdf.columns]
  2834. d = gdf[["INDDID", "NPDx1", outcome, path_col] + covars_here].dropna()
  2835. if len(d) < 8:
  2836. continue
  2837. # Z-score numeric vars
  2838. for col in d.select_dtypes(include=[np.number]).columns:
  2839. if d[col].std(ddof=0) > 0:
  2840. d[col] = (d[col] - d[col].mean()) / d[col].std(ddof=0)
  2841. rhs = [path_col] + covars_here
  2842. formula = f"{outcome} ~ " + " + ".join(rhs)
  2843. res = smf.ols(formula, d).fit()
  2844. # -------------------------------
  2845. # PRIMARY predictor + CI
  2846. # -------------------------------
  2847. coef = res.params.get(path_col, np.nan)
  2848. se = res.bse.get(path_col, np.nan)
  2849. ci_low = coef - 1.96 * se
  2850. ci_high = coef + 1.96 * se
  2851. results_all.append({
  2852. "Group": group_name,
  2853. "Structure": s,
  2854. "Parameter": path_col,
  2855. "Type": "Primary",
  2856. "StdBeta": coef,
  2857. "CI_low": ci_low,
  2858. "CI_high": ci_high,
  2859. "p-value": res.pvalues.get(path_col, np.nan),
  2860. "N": len(d["INDDID"].unique())
  2861. })
  2862. # -------------------------------
  2863. # Covariates + CI
  2864. # -------------------------------
  2865. for cov in covars_here:
  2866. coef = res.params.get(cov, np.nan)
  2867. se = res.bse.get(cov, np.nan)
  2868. ci_low = coef - 1.96 * se
  2869. ci_high = coef + 1.96 * se
  2870. results_all.append({
  2871. "Group": group_name,
  2872. "Structure": s,
  2873. "Parameter": cov,
  2874. "Type": "Covariate",
  2875. "StdBeta": coef,
  2876. "CI_low": ci_low,
  2877. "CI_high": ci_high,
  2878. "p-value": res.pvalues.get(cov, np.nan),
  2879. "N": len(d["INDDID"].unique())
  2880. })
  2881. res_df = pd.DataFrame(results_all)
  2882. # FDR correction
  2883. if not res_df.empty:
  2884. _, pf = pg.multicomp(res_df["p-value"], method="fdr_bh")
  2885. res_df["p-FDR"] = pf
  2886. res_df["Sig(FDR)"] = res_df["p-FDR"].apply(
  2887. lambda p: "***" if p < 0.001 else
  2888. "**" if p < 0.01 else
  2889. "*" if p < 0.05 else ""
  2890. )
  2891. return res_df
  2892. ##########################################################################################
  2893. # 5) Run all groups
  2894. ##########################################################################################
  2895. all_res = []
  2896. for g in disease_groups:
  2897. r = run_primary_lme_by_group(df_use, g, structures)
  2898. if not r.empty:
  2899. all_res.append(r)
  2900. primary_results_df = pd.concat(all_res, ignore_index=True)
  2901. ##########################################################################################
  2902. # 6) Pretty printing
  2903. ##########################################################################################
  2904. marker_clean = {
  2905. "EC_CS_DGTau": "p-tau (EC/CS/DG)",
  2906. "AmygTau": "p-tau (Amygdala)",
  2907. "CPTau": "p-tau (Caudate/Putamen)",
  2908. "TSTau": "p-tau (Thalamus)",
  2909. "GPTau": "p-tau (Pallidum)",
  2910. "EC_CS_DGaSyn": "α-syn (EC/CS/DG)",
  2911. "EC_CS_DGTDP43": "TDP-43 (EC/CS/DG)"
  2912. }
  2913. pretty_cov = {
  2914. "AgeatDeath": "Age at death",
  2915. "Sex": "Sex",
  2916. "Education": "Education",
  2917. "PMI": "PMI"
  2918. }
  2919. def pretty_param(p):
  2920. if p in marker_clean: return marker_clean[p]
  2921. if p in pretty_cov: return pretty_cov[p]
  2922. return p
  2923. struct_pretty = {
  2924. "hippocampus": "Hippocampus",
  2925. "amygdala": "Amygdala",
  2926. "caudate": "Caudate",
  2927. "putamen": "Putamen",
  2928. "thalamus": "Thalamus",
  2929. "pallidum": "Pallidum"
  2930. }
  2931. pretty_group_short = {
  2932. "alzheimer's disease": "AD",
  2933. "lewy body disease": "LBD",
  2934. "ftld-tdp": "FTLD-TDP",
  2935. "tauopathies": "Tauopathies"
  2936. }
  2937. ##########################################################################################
  2938. # 7) Build final table with CI
  2939. ##########################################################################################
  2940. df2 = primary_results_df.copy()
  2941. df2["GroupPrint"] = df2["Group"].map(pretty_group_short)
  2942. df2["StructurePrint"] = df2["Structure"].map(struct_pretty)
  2943. df2["ParameterPrint"] = df2["Parameter"].apply(pretty_param)
  2944. df2["StdBeta_fmt"] = df2["StdBeta"].map(lambda x: f"{x:.3f}")
  2945. df2["CI_fmt"] = df2.apply(lambda r: f"[{r.CI_low:.3f}, {r.CI_high:.3f}]", axis=1)
  2946. df2["p_fmt"] = df2["p-value"].map(lambda x: f"{x:.4f}")
  2947. df2["pFDR_fmt"] = df2["p-FDR"].map(lambda x: f"{x:.4f}")
  2948. df2 = df2.sort_values(["GroupPrint", "StructurePrint", "Type"])
  2949. final_table = df2[[
  2950. "GroupPrint", "StructurePrint", "ParameterPrint",
  2951. "StdBeta_fmt", "CI_fmt", "p_fmt", "pFDR_fmt",
  2952. "Sig(FDR)", "N"
  2953. ]]
  2954. ##########################################################################################
  2955. # 8) DOCX EXPORT
  2956. ##########################################################################################
  2957. doc = Document()
  2958. title = doc.add_heading("Primary Pathology → Postmortem Volume (Standardized LME)", level=1)
  2959. title.alignment = 1
  2960. doc.add_paragraph(
  2961. "Each table reports standardized β, 95% confidence interval (CI), raw p-value, "
  2962. "FDR-corrected p-value, significance code, and sample size."
  2963. )
  2964. doc.add_page_break()
  2965. groups_in_order = ["alzheimer's disease", "lewy body disease", "ftld-tdp", "tauopathies"]
  2966. for g in groups_in_order:
  2967. sub = final_table[final_table["GroupPrint"] == pretty_group_short[g]]
  2968. if sub.empty:
  2969. continue
  2970. h = doc.add_heading(pretty_group[g], level=2)
  2971. tbl = doc.add_table(rows=1, cols=len(final_table.columns))
  2972. tbl.style = "Table Grid"
  2973. hdr = tbl.rows[0].cells
  2974. for j, colname in enumerate(final_table.columns):
  2975. hdr[j].text = colname
  2976. for _, row in sub.iterrows():
  2977. rw = tbl.add_row().cells
  2978. for j, colname in enumerate(final_table.columns):
  2979. rw[j].text = str(row[colname])
  2980. doc.add_page_break()
  2981. output_file = "primary_pathology_LME_results_by_group_with_CI.docx"
  2982. doc.save(output_file)
  2983. print("Saved DOCX:", output_file)
  2984. # %%
  2985. """
  2986. Heatmaps
  2987. """
  2988. def plot_primary_pathology_heatmaps_clean(primary_results_df):
  2989. # -------------------------------------------------------
  2990. # Filter *only PRIMARY PATHOLOGY rows*
  2991. # -------------------------------------------------------
  2992. dfp = primary_results_df[primary_results_df["Type"] == "Primary"].copy()
  2993. # -------------------------------------------------------
  2994. # Structures (ordered)
  2995. # -------------------------------------------------------
  2996. struct_order = ["hippocampus", "amygdala", "caudate",
  2997. "putamen", "thalamus", "pallidum"]
  2998. struct_labels = {s: s.capitalize() for s in struct_order}
  2999. dfp["StructureLabel"] = dfp["Structure"].map(struct_labels)
  3000. # -------------------------------------------------------
  3001. # Pretty pathology names for prefix-based parameters
  3002. # -------------------------------------------------------
  3003. pretty_path = {
  3004. "EC_CS_DGTau": "p-tau",
  3005. "AmygTau": "p-tau",
  3006. "CPTau": "p-tau",
  3007. "TSTau": "p-tau",
  3008. "GPTau": "p-tau",
  3009. "EC_CS_DGaSyn": "α-syn",
  3010. "EC_CS_DGTDP43": "TDP-43"
  3011. }
  3012. # Parameter → pretty label
  3013. dfp["PathLabel"] = dfp["Parameter"].map(pretty_path).fillna("")
  3014. # -------------------------------------------------------
  3015. # Correct group order and labels
  3016. # -------------------------------------------------------
  3017. group_order = [
  3018. "alzheimer's disease",
  3019. "lewy body disease",
  3020. "ftld-tdp",
  3021. "tauopathies"
  3022. ]
  3023. column_labels = [
  3024. "AD\n(p-tau)",
  3025. "LBD\n(α-syn)",
  3026. "FTLD-TDP\n(TDP-43)",
  3027. "Tauopathies\n(p-tau)"
  3028. ]
  3029. # -------------------------------------------------------
  3030. # Create matrices (StdBeta, p, stars)
  3031. # -------------------------------------------------------
  3032. M = []
  3033. P = []
  3034. Stars = []
  3035. for s in struct_order:
  3036. beta_row = []
  3037. p_row = []
  3038. star_row = []
  3039. for grp in group_order:
  3040. sub = dfp[(dfp["Group"] == grp) & (dfp["Structure"] == s)]
  3041. if sub.empty:
  3042. beta_row.append(np.nan)
  3043. p_row.append(np.nan)
  3044. star_row.append("")
  3045. continue
  3046. beta = sub["StdBeta"].values[0]
  3047. pval = sub["p-value"].values[0]
  3048. pfdr = sub["p-FDR"].values[0]
  3049. # stars based on FDR
  3050. if pfdr < 0.001:
  3051. sig = "***"
  3052. elif pfdr < 0.01:
  3053. sig = "**"
  3054. elif pfdr < 0.05:
  3055. sig = "*"
  3056. else:
  3057. sig = ""
  3058. beta_row.append(beta)
  3059. p_row.append(pval)
  3060. star_row.append(sig)
  3061. M.append(beta_row)
  3062. P.append(p_row)
  3063. Stars.append(star_row)
  3064. M = np.array(M)
  3065. P = np.array(P)
  3066. Stars = np.array(Stars)
  3067. # -------------------------------------------------------
  3068. # Colormap
  3069. # -------------------------------------------------------
  3070. base_cmap = sns.color_palette("PRGn", as_cmap=True)
  3071. cmap = colors.LinearSegmentedColormap.from_list(
  3072. "PRGn_soft", base_cmap(np.linspace(0.15, 0.85, 256))
  3073. )
  3074. # -------------------------------------------------------
  3075. # Plot
  3076. # -------------------------------------------------------
  3077. fig, ax = plt.subplots(figsize=(13, 6))
  3078. sns.heatmap(
  3079. M,
  3080. ax=ax,
  3081. cmap=cmap,
  3082. vmin=-1, vmax=1,
  3083. linewidths=0.6,
  3084. linecolor="white",
  3085. cbar=True,
  3086. cbar_kws={"label": "Std β"},
  3087. annot=False
  3088. )
  3089. ax.set_yticks(np.arange(len(struct_order)) + 0.5)
  3090. ax.set_yticklabels([struct_labels[s] for s in struct_order], rotation=0)
  3091. ax.set_xticks(np.arange(len(group_order)) + 0.5)
  3092. ax.set_xticklabels(column_labels, rotation=0, ha="center")
  3093. # -------------------------------------------------------
  3094. # Annotate cells
  3095. # -------------------------------------------------------
  3096. for y in range(M.shape[0]):
  3097. for x in range(M.shape[1]):
  3098. beta = M[y, x]
  3099. pval = P[y, x]
  3100. sig = Stars[y, x]
  3101. if np.isnan(beta):
  3102. continue
  3103. txt = f"β={beta:.2f}{sig}\np={pval:.3f}"
  3104. ax.text(
  3105. x + 0.5,
  3106. y + 0.5,
  3107. txt,
  3108. ha="center",
  3109. va="center",
  3110. fontsize=8.5
  3111. )
  3112. ax.set_title("Primary Pathology → Postmortem Volume (LME)", fontsize=16, pad=18)
  3113. ax.set_ylabel("Brain Structure", fontsize=13)
  3114. plt.tight_layout()
  3115. plt.savefig("primary_pathology_heatmap_clean.png", dpi=600, bbox_inches="tight")
  3116. plt.show()
  3117. # ===================== RUN =====================
  3118. plot_primary_pathology_heatmaps_clean(primary_results_df)
  3119. # %%
  3120. ##########################################################################################
  3121. ##########################################################################################
  3122. ####### Gliosis and NeuronLoss: Mediation Analyses
  3123. ##########################################################################################
  3124. ##########################################################################################
  3125. # %%
  3126. """
  3127. Generates disease-specific partial Spearman correlation plots between gliosis/neuron-loss pathology scores and ICV-normalized postmortem limbic/subcortical volumes.
  3128. For each diagnostic group, pathology marker, and structure, the script adjusts for age at death, sex, PMI, and education, applies FDR correction within each marker–group set, and displays regression plots annotated with ρ, raw p-value, significance stars, and sample size.
  3129. """
  3130. # ---------------------------------------------------------------------
  3131. # USE YOUR CLEANED DF EXACTLY AS IS
  3132. # ---------------------------------------------------------------------
  3133. df_use = df.copy()
  3134. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  3135. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  3136. if "antemortem_icv" not in df_use.columns:
  3137. raise ValueError("Missing 'antemortem_icv' column!")
  3138. # ---------------------------------------------------------------------
  3139. # Structures and mappings
  3140. # ---------------------------------------------------------------------
  3141. structures = ["hippocampus", "amygdala", "caudate",
  3142. "putamen", "thalamus", "pallidum"]
  3143. region_map = {
  3144. "caudate": "CP",
  3145. "putamen": "CP",
  3146. "thalamus": "TS",
  3147. "pallidum": "GP",
  3148. "hippocampus": "EC_CS_DG",
  3149. "amygdala": "Amyg",
  3150. }
  3151. # Normalize postmortem volumes by ICV
  3152. for s in structures:
  3153. pm_col = f"postmortem_{s}"
  3154. if pm_col in df_use.columns:
  3155. df_use[f"{pm_col}_norm"] = df_use[pm_col] / df_use["antemortem_icv"]
  3156. # ---------------------------------------------------------------------
  3157. # Pathology features
  3158. # ---------------------------------------------------------------------
  3159. pathology_features = ["Gliosis", "NeuronLoss"]
  3160. # Your 4 disease groups already harmonized in df_use
  3161. groups = ["alzheimer's disease", "lewy body disease",
  3162. "ftld-tdp", "tauopathies"]
  3163. sns.set(style="whitegrid", context="talk")
  3164. color = "tab:purple"
  3165. # ---------------------------------------------------------------------
  3166. # Run and plot per pathology marker × group
  3167. # ---------------------------------------------------------------------
  3168. for marker_suffix in pathology_features:
  3169. print(f"\n===== {marker_suffix.upper()} =====")
  3170. for g in groups:
  3171. subdf = df_use[df_use["NPDx1"] == g]
  3172. if subdf.empty:
  3173. print(f"[skip] No subjects for group: {g}")
  3174. continue
  3175. print(f"\n--- {g.upper()} ---")
  3176. fig, axes = plt.subplots(
  3177. 1, len(structures), figsize=(6 * len(structures), 5)
  3178. )
  3179. if len(structures) == 1:
  3180. axes = [axes]
  3181. all_stats, all_p = [], []
  3182. # -----------------------------
  3183. # Compute partial Spearman stats
  3184. # -----------------------------
  3185. for i, s in enumerate(structures):
  3186. ax = axes[i]
  3187. post_col = f"postmortem_{s}_norm"
  3188. if post_col not in subdf.columns:
  3189. ax.text(0.5, 0.5, "Missing volume", ha="center", va="center")
  3190. ax.axis("off")
  3191. continue
  3192. # Find pathology column
  3193. prefix = region_map[s]
  3194. path_candidates = [c for c in subdf.columns
  3195. if c.lower().startswith(prefix.lower())]
  3196. path_cols = [
  3197. c for c in path_candidates
  3198. if marker_suffix.lower() in c.lower()
  3199. ]
  3200. if not path_cols:
  3201. ax.text(0.5, 0.5, f"No {prefix}{marker_suffix}",
  3202. ha="center", va="center")
  3203. ax.axis("off")
  3204. continue
  3205. path_col = path_cols[0]
  3206. cols = [path_col, post_col, "AgeatDeath", "Sex", "PMI", "Education"]
  3207. d = subdf[cols].copy().apply(pd.to_numeric, errors="coerce").dropna()
  3208. if len(d) < 5:
  3209. ax.text(0.5, 0.5, "Insufficient data", ha="center", va="center")
  3210. ax.axis("off")
  3211. continue
  3212. res = pg.partial_corr(
  3213. data=d,
  3214. x=path_col, y=post_col,
  3215. covar=["AgeatDeath", "Sex", "PMI", "Education"],
  3216. method="spearman"
  3217. )
  3218. r, p = res["r"].iloc[0], res["p-val"].iloc[0]
  3219. all_stats.append((s, r, p, len(d)))
  3220. all_p.append(p)
  3221. # -----------------------------
  3222. # FDR correction
  3223. # -----------------------------
  3224. if len(all_p) > 0:
  3225. reject, p_corr = pg.multicomp(all_p, method="fdr_bh")
  3226. else:
  3227. reject, p_corr = [], []
  3228. # -----------------------------
  3229. # Final plotting with p_FDR stars
  3230. # -----------------------------
  3231. for (s, r, p, n), pc, rej, ax in zip(all_stats, p_corr, reject, axes):
  3232. post_col = f"postmortem_{s}_norm"
  3233. prefix = region_map[s]
  3234. path_candidates = [c for c in subdf.columns
  3235. if c.lower().startswith(prefix.lower())]
  3236. path_cols = [
  3237. c for c in path_candidates
  3238. if marker_suffix.lower() in c.lower()
  3239. ]
  3240. path_col = path_cols[0]
  3241. d = subdf[
  3242. [path_col, post_col, "AgeatDeath", "Sex", "PMI", "Education"]
  3243. ].copy().apply(pd.to_numeric, errors="coerce").dropna()
  3244. sig = "***" if pc < 0.001 else "**" if pc < 0.01 else "*" if pc < 0.05 else ""
  3245. sns.regplot(
  3246. data=d, x=path_col, y=post_col,
  3247. scatter_kws=dict(alpha=0.7, s=55),
  3248. line_kws=dict(color=color, lw=2),
  3249. color=color, ax=ax
  3250. )
  3251. ax.set_title(
  3252. f"{s.capitalize()} ({marker_suffix})\n"
  3253. f"ρ = {r:.2f}, p = {p:.3f}{sig}, n = {n}",
  3254. fontsize=11, fontweight="bold", pad=10
  3255. )
  3256. ax.set_xlabel(marker_suffix)
  3257. ax.set_ylabel("Postmortem Volume / ICV")
  3258. ax.grid(True, linestyle=":", alpha=0.5)
  3259. plt.suptitle(
  3260. f"{marker_suffix} vs Postmortem Volume — {g.title()} (Partial Spearman, FDR-corrected)",
  3261. fontsize=16, fontweight="bold"
  3262. )
  3263. plt.tight_layout(rect=[0, 0, 1, 0.94])
  3264. plt.show()
  3265. # %%
  3266. ##########################################################################################
  3267. # Final Postmortem Partial Spearman Heatmaps (Gliosis & Neuronal loss)
  3268. ##########################################################################################
  3269. # ---------------------------------------------------------------------
  3270. # Use your cleaned df exactly as-is
  3271. # ---------------------------------------------------------------------
  3272. df_use = df.copy()
  3273. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  3274. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  3275. # ---------------------------------------------------------------------
  3276. # Structures, groups, pathology features
  3277. # ---------------------------------------------------------------------
  3278. structures = ["hippocampus", "amygdala", "caudate", "putamen", "thalamus", "pallidum"]
  3279. region_map = {
  3280. "caudate": "CP",
  3281. "putamen": "CP",
  3282. "thalamus": "TS",
  3283. "pallidum": "GP",
  3284. "hippocampus": "EC_CS_DG",
  3285. "amygdala": "Amyg",
  3286. }
  3287. groups = ["alzheimer's disease", "lewy body disease",
  3288. "ftld-tdp", "tauopathies"]
  3289. group_labels = [
  3290. "Alzheimer's\ndisease", "Lewy body\ndisease",
  3291. "FTLD-TDP", "Tauopathies"
  3292. ]
  3293. pathology_features = [
  3294. ("Gliosis", "Gliosis"),
  3295. ("NeuronLoss", "Neuronal loss")
  3296. ]
  3297. covars = ["AgeatDeath", "Sex", "PMI", "Education"]
  3298. # ---------------------------------------------------------------------
  3299. # Normalize postmortem volumes by ICV
  3300. # ---------------------------------------------------------------------
  3301. if "antemortem_icv" not in df_use.columns:
  3302. raise ValueError("Missing 'antemortem_icv' column!")
  3303. for s in structures:
  3304. pm_col = f"postmortem_{s}"
  3305. if pm_col in df_use.columns:
  3306. df_use[f"{pm_col}_norm"] = df_use[pm_col] / df_use["antemortem_icv"]
  3307. sns.set(style="white")
  3308. # ---------------------------------------------------------------------
  3309. # Compute partial correlations
  3310. # ---------------------------------------------------------------------
  3311. results = []
  3312. for g in groups:
  3313. subdf = df_use[df_use["NPDx1"] == g].copy()
  3314. for marker_col, marker_label in pathology_features:
  3315. for s in structures:
  3316. post_col = f"postmortem_{s}_norm"
  3317. prefix = region_map[s]
  3318. # pathology columns beginning with prefix (EC_CS_DG, Amyg, CP, TS, GP)
  3319. path_candidates = [
  3320. c for c in subdf.columns
  3321. if c.lower().startswith(prefix.lower())
  3322. ]
  3323. # match Gliosis / NeuronLoss case-insensitive
  3324. path_cols = [
  3325. c for c in path_candidates
  3326. if marker_col.lower() in c.lower()
  3327. ]
  3328. if not path_cols or post_col not in subdf.columns:
  3329. continue
  3330. path_col = path_cols[0]
  3331. cols = [path_col, post_col] + covars
  3332. d = subdf[cols].apply(pd.to_numeric, errors="coerce").dropna()
  3333. if len(d) < 5:
  3334. continue
  3335. res = pg.partial_corr(
  3336. data=d, x=path_col, y=post_col,
  3337. covar=covars, method="spearman"
  3338. )
  3339. results.append({
  3340. "Group": g,
  3341. "Pathology": marker_label,
  3342. "Structure": s,
  3343. "r": res["r"].iloc[0],
  3344. "p": res["p-val"].iloc[0]
  3345. })
  3346. res_df = pd.DataFrame(results)
  3347. # ---------------------------------------------------------------------
  3348. # FDR correction within each group × pathology
  3349. # ---------------------------------------------------------------------
  3350. res_df["p_FDR"] = np.nan
  3351. for g in groups:
  3352. for marker_label in [m[1] for m in pathology_features]:
  3353. sub = res_df[(res_df["Group"] == g) &
  3354. (res_df["Pathology"] == marker_label)]
  3355. if len(sub) > 0:
  3356. _, p_corr = pg.multicomp(sub["p"], method="fdr_bh")
  3357. res_df.loc[sub.index, "p_FDR"] = p_corr
  3358. res_df["sig"] = res_df["p_FDR"].apply(
  3359. lambda p: "***" if p < 0.001 else
  3360. "**" if p < 0.01 else
  3361. "*" if p < 0.05 else ""
  3362. )
  3363. # ---------------------------------------------------------------------
  3364. # Plot two side-by-side heatmaps
  3365. # ---------------------------------------------------------------------
  3366. fig, axes = plt.subplots(1, 2, figsize=(13.2, 6.8), sharey=True)
  3367. cmap = sns.color_palette("PRGn", as_cmap=True)
  3368. vmin, vmax = -1, 1
  3369. for ax, marker_label in zip(axes, [m[1] for m in pathology_features]):
  3370. sub = res_df[res_df["Pathology"] == marker_label]
  3371. # Pivot into structure x group matrix
  3372. pivot_r = sub.pivot(index="Structure", columns="Group", values="r")
  3373. pivot_r = pivot_r.reindex(index=structures, columns=groups)
  3374. # Build annotation text matrix
  3375. annot_df = sub.set_index(["Structure", "Group"])
  3376. annot_text = []
  3377. for s in structures:
  3378. row = []
  3379. for g in groups:
  3380. try:
  3381. r = annot_df.loc[(s, g), "r"]
  3382. p = annot_df.loc[(s, g), "p"]
  3383. sig = annot_df.loc[(s, g), "sig"]
  3384. row.append(f"{r:.2f}\n(p={p:.3f}){sig}" if not pd.isna(r) else "")
  3385. except KeyError:
  3386. row.append("")
  3387. annot_text.append(row)
  3388. sns.heatmap(
  3389. pivot_r,
  3390. annot=np.array(annot_text), fmt="",
  3391. cmap=cmap, center=0, vmin=vmin, vmax=vmax,
  3392. linewidths=1, linecolor="white",
  3393. cbar=(ax == axes[-1]),
  3394. cbar_kws={"label": "ρ (partial Spearman)"},
  3395. ax=ax,
  3396. annot_kws={"fontsize": 9, "color": "black"}
  3397. )
  3398. ax.set_title(marker_label, fontsize=12, pad=10, color="black")
  3399. ax.set_xticklabels(group_labels, rotation=0, fontsize=9)
  3400. ax.set_yticklabels([s.capitalize() for s in structures],
  3401. rotation=0, fontsize=9)
  3402. ax.set_xlabel("")
  3403. ax.set_ylabel("")
  3404. # ---------------------------------------------------------------------
  3405. # Unified axis labels
  3406. # ---------------------------------------------------------------------
  3407. fig.text(0.5, 0.035, "Disease group", ha="center",
  3408. fontsize=11, color="black")
  3409. fig.text(0.06, 0.5, "Structure", va="center",
  3410. rotation=90, fontsize=11, color="black")
  3411. # ---------------------------------------------------------------------
  3412. # Main title
  3413. # ---------------------------------------------------------------------
  3414. plt.suptitle(
  3415. "Partial Spearman correlation between postmortem MRI volumes\n"
  3416. "and regional neurodegeneration markers (Gliosis & Neuronal loss)",
  3417. fontsize=14, y=0.98
  3418. )
  3419. plt.tight_layout(rect=[0.06, 0.05, 0.95, 0.93], w_pad=1.2)
  3420. plt.savefig("heatmpa_gliosis_nl.png", dpi=600, bbox_inches="tight")
  3421. plt.show()
  3422. # %%
  3423. ##########################################################################################
  3424. # FINAL — Scatter Plots for Gliosis and Neuronal Loss
  3425. # Same layout as ABeta/CERAD/Braak scatter plots
  3426. # 6 rows (structures) × 4 columns (disease groups)
  3427. # 2 figures total (Gliosis, Neuronal Loss)
  3428. ##########################################################################################
  3429. ##########################################################################################
  3430. # DATA PREP
  3431. ##########################################################################################
  3432. df_use = df.copy()
  3433. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  3434. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  3435. ##########################################################################################
  3436. # STRUCTURES + NORMALIZATION
  3437. ##########################################################################################
  3438. structures = ["hippocampus", "amygdala", "caudate",
  3439. "putamen", "thalamus", "pallidum"]
  3440. pretty_structure = {
  3441. "hippocampus": "Hippocampus",
  3442. "amygdala": "Amygdala",
  3443. "caudate": "Caudate",
  3444. "putamen": "Putamen",
  3445. "thalamus": "Thalamus",
  3446. "pallidum": "Pallidum"
  3447. }
  3448. for s in structures:
  3449. pm = f"postmortem_{s}"
  3450. if pm in df_use.columns:
  3451. df_use[f"{pm}_norm"] = df_use[pm] / df_use["antemortem_icv"]
  3452. ##########################################################################################
  3453. # PATHOLOGY REGIONS (same map as heatmap)
  3454. ##########################################################################################
  3455. region_map = {
  3456. "hippocampus": "EC_CS_DG",
  3457. "amygdala": "Amyg",
  3458. "caudate": "CP",
  3459. "putamen": "CP",
  3460. "thalamus": "TS",
  3461. "pallidum": "GP"
  3462. }
  3463. pathology_features = [
  3464. ("Gliosis", "Gliosis"),
  3465. ("NeuronLoss", "Neuronal loss")
  3466. ]
  3467. ##########################################################################################
  3468. # DISEASE GROUPS + COLORS
  3469. ##########################################################################################
  3470. groups = ["alzheimer's disease", "lewy body disease",
  3471. "ftld-tdp", "tauopathies"]
  3472. pretty_group = {
  3473. "alzheimer's disease": "AD",
  3474. "lewy body disease": "LBD",
  3475. "ftld-tdp": "FTLD-TDP",
  3476. "tauopathies": "Tauopathies"
  3477. }
  3478. group_color = {
  3479. "alzheimer's disease": "#4C72B0",
  3480. "lewy body disease": "#DD8452",
  3481. "ftld-tdp": "#55A868",
  3482. "tauopathies": "#CBAF00"
  3483. }
  3484. ##########################################################################################
  3485. # SUPERSCRIPT ASTERISKS FOR SIGNIFICANCE
  3486. ##########################################################################################
  3487. def p_to_superscript(p_fdr):
  3488. if p_fdr < 0.001:
  3489. return "$^{***}$"
  3490. elif p_fdr < 0.01:
  3491. return "$^{**}$"
  3492. elif p_fdr < 0.05:
  3493. return "$^{*}$"
  3494. else:
  3495. return ""
  3496. ##########################################################################################
  3497. # MAIN LOOP — TWO FIGURES (Gliosis, Neuronal loss)
  3498. ##########################################################################################
  3499. for marker_col, marker_label in pathology_features:
  3500. fig, axes = plt.subplots(
  3501. len(structures), len(groups),
  3502. figsize=(4.2 * len(groups), 3.0 * len(structures)),
  3503. sharex=False, sharey=False
  3504. )
  3505. # For each disease group
  3506. for col, g in enumerate(groups):
  3507. gdf = df_use[df_use["NPDx1"] == g].copy()
  3508. # FIRST PASS: gather raw p-values for FDR
  3509. raw_pvals = []
  3510. for s in structures:
  3511. post_col = f"postmortem_{s}_norm"
  3512. prefix = region_map[s]
  3513. # find pathology column
  3514. path_candidates = [c for c in gdf.columns if c.lower().startswith(prefix.lower())]
  3515. path_cols = [c for c in path_candidates if marker_col.lower() in c.lower()]
  3516. if not path_cols or post_col not in gdf.columns:
  3517. raw_pvals.append(np.nan)
  3518. continue
  3519. path_col = path_cols[0]
  3520. cols = [path_col, post_col, "AgeatDeath", "Sex", "PMI", "Education"]
  3521. d = gdf[cols].apply(pd.to_numeric, errors="coerce").dropna()
  3522. if len(d) < 5:
  3523. raw_pvals.append(np.nan)
  3524. continue
  3525. res = pg.partial_corr(
  3526. data=d, x=path_col, y=post_col,
  3527. covar=["AgeatDeath", "Sex", "PMI", "Education"],
  3528. method="spearman"
  3529. )
  3530. raw_pvals.append(res["p-val"].iloc[0])
  3531. # FDR — within group × pathology
  3532. valid_mask = ~pd.isna(raw_pvals)
  3533. valid_p = np.array(raw_pvals)[valid_mask]
  3534. if len(valid_p) > 0:
  3535. _, p_fdr_valid = pg.multicomp(valid_p, method="fdr_bh")
  3536. else:
  3537. p_fdr_valid = []
  3538. # Expand back
  3539. p_fdr_full = []
  3540. idx = 0
  3541. for v in valid_mask:
  3542. if v:
  3543. p_fdr_full.append(p_fdr_valid[idx])
  3544. idx += 1
  3545. else:
  3546. p_fdr_full.append(np.nan)
  3547. # SECOND PASS: plotting
  3548. for row, s in enumerate(structures):
  3549. ax = axes[row, col]
  3550. # Remove seaborn y-label
  3551. ax.set_ylabel("")
  3552. if col == 0:
  3553. ax.set_ylabel(pretty_structure[s], fontsize=13)
  3554. post_col = f"postmortem_{s}_norm"
  3555. prefix = region_map[s]
  3556. path_candidates = [c for c in gdf.columns if c.lower().startswith(prefix.lower())]
  3557. path_cols = [c for c in path_candidates if marker_col.lower() in c.lower()]
  3558. if not path_cols or post_col not in gdf.columns:
  3559. ax.text(0.5, 0.5, "No data", ha="center", va="center")
  3560. ax.set_xticks([]); ax.set_yticks([])
  3561. continue
  3562. path_col = path_cols[0]
  3563. cols = [path_col, post_col, "AgeatDeath", "Sex", "PMI", "Education"]
  3564. d = gdf[cols].apply(pd.to_numeric, errors="coerce").dropna()
  3565. if len(d) < 5:
  3566. ax.text(0.5, 0.5, "N too small", ha="center", va="center")
  3567. ax.set_xticks([]); ax.set_yticks([])
  3568. continue
  3569. # Stats
  3570. res = pg.partial_corr(
  3571. data=d, x=path_col, y=post_col,
  3572. covar=["AgeatDeath", "Sex", "PMI", "Education"],
  3573. method="spearman"
  3574. )
  3575. r = res["r"].iloc[0]
  3576. p_raw = res["p-val"].iloc[0]
  3577. p_fdr = p_fdr_full[row]
  3578. sup = p_to_superscript(p_fdr)
  3579. # Plot
  3580. sns.regplot(
  3581. data=d,
  3582. x=path_col,
  3583. y=post_col,
  3584. scatter_kws=dict(color=group_color[g], s=50, alpha=0.75),
  3585. line_kws=dict(color=group_color[g], lw=2.3, alpha=0.9),
  3586. ax=ax
  3587. )
  3588. # Clean axis after regplot overwrites
  3589. ax.set_ylabel("")
  3590. if col == 0:
  3591. ax.set_ylabel(pretty_structure[s], fontsize=13)
  3592. # X-label only bottom row
  3593. if row == len(structures)-1:
  3594. ax.set_xlabel(marker_label, fontsize=12)
  3595. else:
  3596. ax.set_xlabel("")
  3597. ax.set_title(
  3598. f"{pretty_group[g]} (ρ = {r:.2f}; p = {p_raw:.3f}{sup})",
  3599. fontsize=12, pad=6
  3600. )
  3601. ax.grid(True, linestyle=":", alpha=0.5)
  3602. # FIGURE TITLE
  3603. plt.suptitle(
  3604. f"Partial Spearman correlations — {marker_label}",
  3605. fontsize=18, weight="bold"
  3606. )
  3607. plt.tight_layout(rect=[0, 0, 1, 0.95])
  3608. # SAVE PNG
  3609. png_name = f"Scatter_{marker_col}.png"
  3610. plt.savefig(png_name, dpi=300, bbox_inches="tight")
  3611. print(f"SAVED PNG: {png_name}")
  3612. # SAVE PDF
  3613. pdf_name = f"Scatter_{marker_col}.pdf"
  3614. with PdfPages(pdf_name) as pdf:
  3615. pdf.savefig(fig, bbox_inches="tight")
  3616. print(f"SAVED PDF: {pdf_name}")
  3617. plt.show()
  3618. print("\n✔✔✔ ALL SCATTER FIGURES DONE")
  3619. # %%
  3620. # %%
  3621. ##########################################################################################
  3622. # OLS heatmaps (Gliosis & Neuronal loss): STANDARDIZED β as color + annotation
  3623. # Annotation: β_std + FDR stars, raw p shown
  3624. # df_use version — NO regrouping, NO relabeling, PLOT ONLY
  3625. ##########################################################################################
  3626. import pandas as pd, numpy as np, seaborn as sns, matplotlib.pyplot as plt
  3627. import statsmodels.api as sm
  3628. import pingouin as pg, warnings, matplotlib
  3629. matplotlib.rcParams['font.family'] = 'DejaVu Sans'
  3630. warnings.filterwarnings("ignore", category=RuntimeWarning)
  3631. np.seterr(divide='ignore', invalid='ignore')
  3632. # ---------------------------------------------------------------------
  3633. # Use your cleaned df exactly as-is
  3634. # ---------------------------------------------------------------------
  3635. df_use = df.copy()
  3636. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  3637. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  3638. # ---------------------------------------------------------------------
  3639. # Structures, groups, pathology features
  3640. # ---------------------------------------------------------------------
  3641. structures = ["hippocampus", "amygdala", "caudate", "putamen", "thalamus", "pallidum"]
  3642. region_map = {
  3643. "caudate": "CP",
  3644. "putamen": "CP",
  3645. "thalamus": "TS",
  3646. "pallidum": "GP",
  3647. "hippocampus": "EC_CS_DG",
  3648. "amygdala": "Amyg",
  3649. }
  3650. groups = ["alzheimer's disease", "lewy body disease", "ftld-tdp", "tauopathies"]
  3651. group_labels = [
  3652. "AD",
  3653. "LBD",
  3654. "FTLD-TDP",
  3655. "FTLD-Tau"
  3656. ]
  3657. pathology_features = [
  3658. ("Gliosis", "Gliosis"),
  3659. ("NeuronLoss", "Neuronal loss")
  3660. ]
  3661. covars = ["AgeatDeath", "Sex", "PMI", "Education"]
  3662. # ---------------------------------------------------------------------
  3663. # Normalize postmortem volumes by ICV
  3664. # ---------------------------------------------------------------------
  3665. if "antemortem_icv" not in df_use.columns:
  3666. raise ValueError("Missing 'antemortem_icv' column!")
  3667. for s in structures:
  3668. pm_col = f"postmortem_{s}"
  3669. if pm_col in df_use.columns:
  3670. df_use[f"{pm_col}_norm"] = df_use[pm_col] / df_use["antemortem_icv"]
  3671. sns.set(style="white")
  3672. # ---------------------------------------------------------------------
  3673. # Fit OLS per group × pathology × structure
  3674. # RAW model is optional; we compute it but PLOT standardized β
  3675. # Standardized β computed by z-scoring y and x (pathology) within subset d
  3676. # ---------------------------------------------------------------------
  3677. results = []
  3678. for g in groups:
  3679. subdf = df_use[df_use["NPDx1"] == g].copy()
  3680. for marker_col, marker_label in pathology_features:
  3681. for s in structures:
  3682. y_col = f"postmortem_{s}_norm"
  3683. prefix = region_map[s]
  3684. # candidates: columns beginning with prefix
  3685. path_candidates = [c for c in subdf.columns if c.lower().startswith(prefix.lower())]
  3686. # match marker substring case-insensitive
  3687. path_cols = [c for c in path_candidates if marker_col.lower() in c.lower()]
  3688. if (not path_cols) or (y_col not in subdf.columns):
  3689. continue
  3690. x_col = path_cols[0]
  3691. cols = [x_col, y_col] + covars
  3692. d = subdf[cols].apply(pd.to_numeric, errors="coerce").dropna()
  3693. if len(d) < 6:
  3694. continue
  3695. # ---- RAW fit (kept for reference/debug) ----
  3696. X = sm.add_constant(d[[x_col] + covars], has_constant="add")
  3697. y = d[y_col].astype(float)
  3698. # ---- Standardized fit (what we plot) ----
  3699. d_std = d.copy()
  3700. x_sd = d_std[x_col].std(ddof=0)
  3701. y_sd = d_std[y_col].std(ddof=0)
  3702. beta_raw, p_raw, beta_std = np.nan, np.nan, np.nan
  3703. try:
  3704. fit = sm.OLS(y, X).fit()
  3705. beta_raw = float(fit.params.get(x_col, np.nan))
  3706. p_raw = float(fit.pvalues.get(x_col, np.nan))
  3707. except Exception:
  3708. pass
  3709. # standardize only if variability exists
  3710. if (x_sd is not None) and (y_sd is not None) and (x_sd > 0) and (y_sd > 0):
  3711. d_std[x_col] = (d_std[x_col] - d_std[x_col].mean()) / x_sd
  3712. d_std[y_col] = (d_std[y_col] - d_std[y_col].mean()) / y_sd
  3713. X_std = sm.add_constant(d_std[[x_col] + covars], has_constant="add")
  3714. y_std = d_std[y_col].astype(float)
  3715. try:
  3716. fit_std = sm.OLS(y_std, X_std).fit()
  3717. beta_std = float(fit_std.params.get(x_col, np.nan))
  3718. except Exception:
  3719. beta_std = np.nan
  3720. results.append({
  3721. "Group": g,
  3722. "Pathology": marker_label,
  3723. "Structure": s,
  3724. "PathCol": x_col,
  3725. "beta": beta_raw,
  3726. "beta_std": beta_std, # <-- PLOTTED
  3727. "p": p_raw, # <-- RAW p (for display + FDR)
  3728. "n": int(len(d))
  3729. })
  3730. res_df = pd.DataFrame(results)
  3731. # ---------------------------------------------------------------------
  3732. # FDR correction within each group × pathology (across structures)
  3733. # Stars from FDR; raw p printed
  3734. # ---------------------------------------------------------------------
  3735. res_df["p_FDR"] = np.nan
  3736. for g in groups:
  3737. for marker_label in [m[1] for m in pathology_features]:
  3738. sub = res_df[(res_df["Group"] == g) &
  3739. (res_df["Pathology"] == marker_label) &
  3740. res_df["p"].notna()]
  3741. if len(sub) > 0:
  3742. _, p_corr = pg.multicomp(sub["p"].values, method="fdr_bh")
  3743. res_df.loc[sub.index, "p_FDR"] = p_corr
  3744. res_df["sig"] = res_df["p_FDR"].apply(
  3745. lambda p: "***" if pd.notna(p) and p < 0.001 else
  3746. "**" if pd.notna(p) and p < 0.01 else
  3747. "*" if pd.notna(p) and p < 0.05 else ""
  3748. )
  3749. # ---------------------------------------------------------------------
  3750. # Plot two side-by-side heatmaps (color = STANDARDIZED β)
  3751. # Annotation: β_std + FDR stars, raw p shown
  3752. # ---------------------------------------------------------------------
  3753. fig, axes = plt.subplots(1, 2, figsize=(13.2, 6.8), sharey=True)
  3754. cmap = sns.color_palette("PRGn", as_cmap=True)
  3755. # symmetric limits based on standardized betas
  3756. betas_std = res_df["beta_std"].replace([np.inf, -np.inf], np.nan).dropna()
  3757. if len(betas_std) == 0:
  3758. raise ValueError("No standardized betas to plot (beta_std is empty). Check model fits / variance.")
  3759. v = float(np.nanmax(np.abs(betas_std.values)))
  3760. vmin, vmax = -v, v
  3761. for ax, marker_label in zip(axes, [m[1] for m in pathology_features]):
  3762. sub = res_df[res_df["Pathology"] == marker_label].copy()
  3763. # heatmap values = standardized β
  3764. pivot_b = sub.pivot(index="Structure", columns="Group", values="beta_std")
  3765. pivot_b = pivot_b.reindex(index=structures, columns=groups)
  3766. annot_df = sub.set_index(["Structure", "Group"])
  3767. annot_text = []
  3768. for s in structures:
  3769. row = []
  3770. for g in groups:
  3771. try:
  3772. bs = annot_df.loc[(s, g), "beta_std"]
  3773. p = annot_df.loc[(s, g), "p"] # RAW p
  3774. sig = annot_df.loc[(s, g), "sig"] # from FDR
  3775. if pd.isna(bs) or pd.isna(p):
  3776. row.append("")
  3777. else:
  3778. row.append(f"β={bs:.2f}{sig}\n(p={p:.3f})")
  3779. except KeyError:
  3780. row.append("")
  3781. annot_text.append(row)
  3782. sns.heatmap(
  3783. pivot_b,
  3784. annot=np.array(annot_text), fmt="",
  3785. cmap=cmap, center=0, vmin=-1.0, vmax=1.0,
  3786. linewidths=1, linecolor="white",
  3787. cbar=(ax == axes[-1]),
  3788. cbar_kws={
  3789. "label": "Std β"
  3790. },
  3791. ax=ax,
  3792. annot_kws={"fontsize": 16, "color": "black"}
  3793. )
  3794. ax.set_title(marker_label, fontsize=12, pad=10, color="black")
  3795. ax.set_xticklabels(group_labels, rotation=0, fontsize=9)
  3796. ax.set_yticklabels([s.capitalize() for s in structures], rotation=0, fontsize=9)
  3797. ax.set_xlabel("")
  3798. ax.set_ylabel("")
  3799. # unified labels
  3800. #fig.text(0.5, 0.035, "Disease group", ha="center", fontsize=12, color="black")
  3801. #fig.text(0.06, 0.5, "Structure", va="center", rotation=90, fontsize=11, color="black")
  3802. plt.suptitle(
  3803. "OLS association between postmortem MRI volumes (ICV-normalized)",
  3804. fontsize=13, y=0.98
  3805. )
  3806. plt.tight_layout(rect=[0.06, 0.05, 0.95, 0.93], w_pad=1.2)
  3807. plt.savefig("heatmap_OLS_stdBeta_gliosis_nl.png", dpi=600, bbox_inches="tight")
  3808. plt.show()
  3809. # %%
  3810. ##########################################################################################
  3811. # DOCX TABLE EXPORT — GLIOSIS & NEURONAL LOSS
  3812. ##########################################################################################
  3813. # ---------------------------------------------------------------------
  3814. # GLOBAL SETTINGS
  3815. # ---------------------------------------------------------------------
  3816. matplotlib.rcParams["font.family"] = "DejaVu Sans"
  3817. warnings.filterwarnings("ignore", category=RuntimeWarning)
  3818. np.seterr(divide="ignore", invalid="ignore")
  3819. # ---------------------------------------------------------------------
  3820. # CLEAN INPUT DATA
  3821. # ---------------------------------------------------------------------
  3822. df_use = df.copy()
  3823. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  3824. df_use["Sex"] = df_use["Sex"].astype("category").cat.codes
  3825. # ---------------------------------------------------------------------
  3826. # STRUCTURES / GROUPS / PATHOLOGY FEATURES
  3827. # ---------------------------------------------------------------------
  3828. structures = [
  3829. "hippocampus",
  3830. "amygdala",
  3831. "caudate",
  3832. "putamen",
  3833. "thalamus",
  3834. "pallidum"
  3835. ]
  3836. region_map = {
  3837. "caudate": "CP",
  3838. "putamen": "CP",
  3839. "thalamus": "TS",
  3840. "pallidum": "GP",
  3841. "hippocampus": "EC_CS_DG",
  3842. "amygdala": "Amyg",
  3843. }
  3844. groups = [
  3845. "alzheimer's disease",
  3846. "lewy body disease",
  3847. "ftld-tdp",
  3848. "tauopathies"
  3849. ]
  3850. group_labels = {
  3851. "alzheimer's disease": "AD",
  3852. "lewy body disease": "LBD",
  3853. "ftld-tdp": "FTLD-TDP",
  3854. "tauopathies": "FTLD-Tau"
  3855. }
  3856. pathology_features = [
  3857. ("Gliosis", "Gliosis"),
  3858. ("NeuronLoss", "Neuronal loss")
  3859. ]
  3860. covars = [
  3861. "AgeatDeath",
  3862. "Sex",
  3863. "PMI",
  3864. "Education"
  3865. ]
  3866. # ---------------------------------------------------------------------
  3867. # NORMALIZE POSTMORTEM VOLUMES BY ICV
  3868. # ---------------------------------------------------------------------
  3869. if "antemortem_icv" not in df_use.columns:
  3870. raise ValueError("Missing 'antemortem_icv' column!")
  3871. for s in structures:
  3872. pm_col = f"postmortem_{s}"
  3873. if pm_col in df_use.columns:
  3874. df_use[f"{pm_col}_norm"] = df_use[pm_col] / df_use["antemortem_icv"]
  3875. # ---------------------------------------------------------------------
  3876. # PRETTY NAMES
  3877. # ---------------------------------------------------------------------
  3878. struct_pretty = {
  3879. "hippocampus": "Hippocampus",
  3880. "amygdala": "Amygdala",
  3881. "caudate": "Caudate",
  3882. "putamen": "Putamen",
  3883. "thalamus": "Thalamus",
  3884. "pallidum": "Pallidum"
  3885. }
  3886. pretty_cov = {
  3887. "AgeatDeath": "Age at death",
  3888. "Sex": "Sex",
  3889. "PMI": "PMI",
  3890. "Education": "Education"
  3891. }
  3892. def pretty_param_from_pathcol(pathcol):
  3893. x = str(pathcol)
  3894. region_pretty = {
  3895. "EC_CS_DG": "EC/CS/DG",
  3896. "Amyg": "Amygdala",
  3897. "CP": "Caudate/Putamen",
  3898. "TS": "Thalamus",
  3899. "GP": "Pallidum",
  3900. }
  3901. marker_pretty = {
  3902. "Gliosis": "Gliosis",
  3903. "NeuronLoss": "Neuronal loss"
  3904. }
  3905. region_name = None
  3906. marker_name = None
  3907. for rp in region_pretty:
  3908. if x.lower().startswith(rp.lower()):
  3909. region_name = region_pretty[rp]
  3910. break
  3911. for mk in marker_pretty:
  3912. if mk.lower() in x.lower():
  3913. marker_name = marker_pretty[mk]
  3914. break
  3915. if region_name is not None and marker_name is not None:
  3916. return f"{marker_name} ({region_name})"
  3917. return x
  3918. def fdr_star(p):
  3919. if pd.isna(p):
  3920. return ""
  3921. if p < 0.001:
  3922. return "***"
  3923. if p < 0.01:
  3924. return "**"
  3925. if p < 0.05:
  3926. return "*"
  3927. return ""
  3928. # ---------------------------------------------------------------------
  3929. # Z-SCORE HELPER
  3930. # ---------------------------------------------------------------------
  3931. def zscore_subset(df_in, cols):
  3932. out = df_in.copy()
  3933. for c in cols:
  3934. sd = out[c].std(ddof=0)
  3935. if pd.isna(sd) or sd == 0:
  3936. out[c] = np.nan
  3937. else:
  3938. out[c] = (out[c] - out[c].mean()) / sd
  3939. return out
  3940. # ---------------------------------------------------------------------
  3941. # FIT OLS FOR EACH GROUP × PATHOLOGY × STRUCTURE
  3942. # ---------------------------------------------------------------------
  3943. results_all = []
  3944. MIN_N = 6
  3945. for g in groups:
  3946. subdf = df_use[df_use["NPDx1"] == g].copy()
  3947. for marker_col, marker_label in pathology_features:
  3948. for s in structures:
  3949. y_col = f"postmortem_{s}_norm"
  3950. prefix = region_map[s]
  3951. path_candidates = [
  3952. c for c in subdf.columns
  3953. if c.lower().startswith(prefix.lower())
  3954. ]
  3955. path_cols = [
  3956. c for c in path_candidates
  3957. if marker_col.lower() in c.lower()
  3958. ]
  3959. if (not path_cols) or (y_col not in subdf.columns):
  3960. continue
  3961. x_col = path_cols[0]
  3962. cols = [x_col, y_col] + covars
  3963. d = subdf[cols].apply(pd.to_numeric, errors="coerce").dropna()
  3964. if len(d) < MIN_N:
  3965. continue
  3966. dz = zscore_subset(d, [x_col, y_col] + covars).dropna()
  3967. if len(dz) < MIN_N:
  3968. continue
  3969. X = sm.add_constant(dz[[x_col] + covars], has_constant="add")
  3970. y = dz[y_col].astype(float)
  3971. try:
  3972. fit = sm.OLS(y, X).fit()
  3973. except Exception:
  3974. continue
  3975. # ---------------------------------------------------------
  3976. # Primary pathology row
  3977. # ---------------------------------------------------------
  3978. coef = fit.params.get(x_col, np.nan)
  3979. se = fit.bse.get(x_col, np.nan)
  3980. ci_low = coef - 1.96 * se
  3981. ci_high = coef + 1.96 * se
  3982. p_raw = fit.pvalues.get(x_col, np.nan)
  3983. results_all.append({
  3984. "Group": g,
  3985. "Pathology": marker_label,
  3986. "Structure": s,
  3987. "Parameter": x_col,
  3988. "Type": "Primary",
  3989. "StdBeta": float(coef),
  3990. "SE": float(se),
  3991. "CI_low": float(ci_low),
  3992. "CI_high": float(ci_high),
  3993. "p_raw": float(p_raw),
  3994. "N": int(len(dz))
  3995. })
  3996. # ---------------------------------------------------------
  3997. # Covariate rows
  3998. # ---------------------------------------------------------
  3999. for cov in covars:
  4000. coef = fit.params.get(cov, np.nan)
  4001. se = fit.bse.get(cov, np.nan)
  4002. ci_low = coef - 1.96 * se
  4003. ci_high = coef + 1.96 * se
  4004. p_raw = fit.pvalues.get(cov, np.nan)
  4005. results_all.append({
  4006. "Group": g,
  4007. "Pathology": marker_label,
  4008. "Structure": s,
  4009. "Parameter": cov,
  4010. "Type": "Covariate",
  4011. "StdBeta": float(coef),
  4012. "SE": float(se),
  4013. "CI_low": float(ci_low),
  4014. "CI_high": float(ci_high),
  4015. "p_raw": float(p_raw),
  4016. "N": int(len(dz))
  4017. })
  4018. res_df = pd.DataFrame(results_all)
  4019. if res_df.empty:
  4020. raise ValueError("No results were generated. Check input columns, group labels, and minimum sample size.")
  4021. # ---------------------------------------------------------------------
  4022. # FDR CORRECTION FOR ALL ROWS
  4023. #
  4024. # Primary rows corrected together within group × pathology.
  4025. # Covariate rows corrected together within group × pathology.
  4026. #
  4027. # This gives p(FDR) for every row.
  4028. # ---------------------------------------------------------------------
  4029. res_df["p_FDR"] = np.nan
  4030. for g in groups:
  4031. for marker_label in [m[1] for m in pathology_features]:
  4032. for row_type in ["Primary", "Covariate"]:
  4033. idx = (
  4034. (res_df["Group"] == g) &
  4035. (res_df["Pathology"] == marker_label) &
  4036. (res_df["Type"] == row_type) &
  4037. res_df["p_raw"].notna()
  4038. )
  4039. pvals = res_df.loc[idx, "p_raw"].values
  4040. if len(pvals) > 0:
  4041. _, p_corr = pg.multicomp(pvals, method="fdr_bh")
  4042. res_df.loc[idx, "p_FDR"] = p_corr
  4043. res_df["Sig(FDR)"] = res_df["p_FDR"].apply(fdr_star)
  4044. # ---------------------------------------------------------------------
  4045. # PRETTY TABLE
  4046. # ---------------------------------------------------------------------
  4047. table_df = res_df.copy()
  4048. table_df["GroupPrint"] = table_df["Group"].map(group_labels)
  4049. table_df["StructurePrint"] = table_df["Structure"].map(struct_pretty)
  4050. table_df["ParameterPrint"] = table_df["Parameter"].apply(
  4051. lambda x: pretty_cov[x] if x in pretty_cov else pretty_param_from_pathcol(x)
  4052. )
  4053. table_df["StdBeta_fmt"] = table_df["StdBeta"].map(lambda x: f"{x:.3f}")
  4054. table_df["SE_fmt"] = table_df["SE"].map(lambda x: f"{x:.3f}")
  4055. table_df["CI_fmt"] = table_df.apply(
  4056. lambda r: f"[{r.CI_low:.3f}, {r.CI_high:.3f}]",
  4057. axis=1
  4058. )
  4059. table_df["p_fmt"] = table_df["p_raw"].map(lambda x: f"{x:.4f}")
  4060. table_df["pFDR_fmt"] = table_df["p_FDR"].map(lambda x: f"{x:.4f}" if pd.notna(x) else "")
  4061. table_df["N_fmt"] = table_df["N"].astype(int).astype(str)
  4062. final_table = table_df[[
  4063. "GroupPrint",
  4064. "Pathology",
  4065. "StructurePrint",
  4066. "ParameterPrint",
  4067. "Type",
  4068. "StdBeta_fmt",
  4069. "SE_fmt",
  4070. "CI_fmt",
  4071. "p_fmt",
  4072. "pFDR_fmt",
  4073. "Sig(FDR)",
  4074. "N_fmt"
  4075. ]].sort_values([
  4076. "GroupPrint",
  4077. "Pathology",
  4078. "StructurePrint",
  4079. "Type",
  4080. "ParameterPrint"
  4081. ])
  4082. final_table = final_table.rename(columns={
  4083. "GroupPrint": "Group",
  4084. "StructurePrint": "Structure",
  4085. "ParameterPrint": "Predictor",
  4086. "StdBeta_fmt": "β",
  4087. "SE_fmt": "SE",
  4088. "CI_fmt": "CI (95%)",
  4089. "p_fmt": "p(raw)",
  4090. "pFDR_fmt": "p(FDR)",
  4091. "N_fmt": "N"
  4092. })
  4093. # ---------------------------------------------------------------------
  4094. # DOCX EXPORT — ONE TABLE PER GROUP
  4095. # ---------------------------------------------------------------------
  4096. doc = Document()
  4097. title = doc.add_heading(
  4098. "Gliosis and Neuronal Loss → Postmortem Volume (Standardized OLS)",
  4099. level=1
  4100. )
  4101. title.alignment = WD_ALIGN_PARAGRAPH.CENTER
  4102. doc.add_paragraph(
  4103. "Each table reports standardized β, standard error (SE), 95% confidence interval (CI), "
  4104. "raw p-value, FDR-corrected p-value, significance code, and sample size. "
  4105. "FDR correction is applied to all rows, including covariates, within each disease group "
  4106. "× pathology × row-type block."
  4107. )
  4108. doc.add_page_break()
  4109. group_order = [
  4110. "alzheimer's disease",
  4111. "lewy body disease",
  4112. "ftld-tdp",
  4113. "tauopathies"
  4114. ]
  4115. for g in group_order:
  4116. gp = group_labels[g]
  4117. sub = final_table[final_table["Group"] == gp].copy()
  4118. if sub.empty:
  4119. continue
  4120. h = doc.add_heading(gp, level=2)
  4121. h.alignment = WD_ALIGN_PARAGRAPH.LEFT
  4122. tbl = doc.add_table(rows=1, cols=len(sub.columns))
  4123. tbl.style = "Table Grid"
  4124. # Header row
  4125. hdr = tbl.rows[0].cells
  4126. for j, colname in enumerate(sub.columns):
  4127. hdr[j].text = str(colname)
  4128. # Body rows
  4129. for _, row in sub.iterrows():
  4130. rw = tbl.add_row().cells
  4131. for j, colname in enumerate(sub.columns):
  4132. rw[j].text = str(row[colname])
  4133. doc.add_page_break()
  4134. docx_output = "gliosis_neuronloss_OLS_results_by_group_WITH_SE_AND_ALL_FDR.docx"
  4135. doc.save(docx_output)
  4136. print("Saved DOCX:", docx_output)
  4137. # %%
  4138. ##########################################################################################
  4139. # Postmortem-only Monte Carlo Mediation Analysis
  4140. ##########################################################################################
  4141. import pandas as pd, numpy as np, seaborn as sns, matplotlib.pyplot as plt, pingouin as pg
  4142. from statsmodels.formula.api import ols
  4143. import warnings
  4144. warnings.filterwarnings("ignore", category=RuntimeWarning)
  4145. np.seterr(divide='ignore', invalid='ignore')
  4146. # ---------------------------------------------------------------------
  4147. # Use your cleaned dataframe exactly as-is
  4148. # ---------------------------------------------------------------------
  4149. df_use = df.copy()
  4150. # Ensure clean NPDx1 and numeric postmortem volumes
  4151. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  4152. for c in df_use.columns:
  4153. if c.startswith("postmortem_"):
  4154. df_use[c] = pd.to_numeric(df_use[c], errors="coerce")
  4155. # ---------------------------------------------------------------------
  4156. # Settings
  4157. # ---------------------------------------------------------------------
  4158. disease_groups = ["alzheimer's disease", "ftld-tdp", "lewy body disease", "tauopathies"]
  4159. region_map = {
  4160. "hippocampus": "EC_CS_DG",
  4161. "amygdala": "Amyg",
  4162. "caudate": "CP",
  4163. "putamen": "CP",
  4164. "thalamus": "TS",
  4165. "pallidum": "GP"
  4166. }
  4167. covars = ["AgeatDeath", "Sex", "Education", "PMI"]
  4168. mediators = ["NeuronLoss", "Gliosis"]
  4169. # ---------------------------------------------------------------------
  4170. # Monte Carlo bootstrap function
  4171. # ---------------------------------------------------------------------
  4172. def montecarlo_indirect(df_in, path_col, med_col, vol_col, covars, n_iter=5000, seed=42):
  4173. np.random.seed(seed)
  4174. try:
  4175. # a-path
  4176. a_mod = ols(f"{med_col} ~ {path_col} + {' + '.join(covars)}", data=df_in).fit()
  4177. # b-path
  4178. b_mod = ols(f"{vol_col} ~ {path_col} + {med_col} + {' + '.join(covars)}", data=df_in).fit()
  4179. # total effect
  4180. c_mod = ols(f"{vol_col} ~ {path_col} + {' + '.join(covars)}", data=df_in).fit()
  4181. a = a_mod.params.get(path_col, np.nan)
  4182. b = b_mod.params.get(med_col, np.nan)
  4183. c_prime = b_mod.params.get(path_col, np.nan)
  4184. c_total = c_mod.params.get(path_col, np.nan)
  4185. a_draws = np.random.normal(a, a_mod.bse.get(path_col, np.nan), n_iter)
  4186. b_draws = np.random.normal(b, b_mod.bse.get(med_col, np.nan), n_iter)
  4187. ab_samples = a_draws * b_draws
  4188. indirect = np.mean(ab_samples)
  4189. ci_low, ci_high = np.percentile(ab_samples, [2.5, 97.5])
  4190. # two-sided Monte Carlo p-value
  4191. p_val = 2 * min(np.mean(ab_samples < 0), np.mean(ab_samples > 0))
  4192. prop_med = (indirect / c_total) if c_total not in [0, np.nan] else np.nan
  4193. return c_prime, indirect, prop_med, ci_low, ci_high, p_val
  4194. except Exception:
  4195. return [np.nan]*6
  4196. # ---------------------------------------------------------------------
  4197. # Run postmortem mediation analysis
  4198. # ---------------------------------------------------------------------
  4199. results = []
  4200. for dx in disease_groups:
  4201. gdf = df_use[df_use["NPDx1"] == dx].copy()
  4202. if gdf.empty:
  4203. continue
  4204. # pathology suffix selection
  4205. if dx == "alzheimer's disease":
  4206. suffix = "Tau"
  4207. elif dx == "ftld-tdp":
  4208. suffix = "TDP43"
  4209. elif dx == "lewy body disease":
  4210. suffix = "aSyn"
  4211. elif dx == "tauopathies":
  4212. suffix = "Tau"
  4213. # loop structures
  4214. for s, prefix in region_map.items():
  4215. path_col = f"{prefix}{suffix}"
  4216. for med in mediators:
  4217. med_col = f"{prefix}{med}"
  4218. vol_col = f"postmortem_{s}"
  4219. # must exist
  4220. if not (path_col in gdf.columns and med_col in gdf.columns and vol_col in gdf.columns):
  4221. continue
  4222. cols = [path_col, med_col, vol_col] + covars
  4223. d = gdf[cols].apply(pd.to_numeric, errors="coerce").dropna()
  4224. if len(d) < 15:
  4225. continue
  4226. # z-score all continuous variables
  4227. for c in cols:
  4228. if d[c].std(ddof=0) > 0:
  4229. d[c] = (d[c] - d[c].mean()) / d[c].std(ddof=0)
  4230. cprime, indirect, prop, cil, cih, pv = montecarlo_indirect(
  4231. d, path_col, med_col, vol_col, covars
  4232. )
  4233. if np.isnan(indirect):
  4234. continue
  4235. results.append({
  4236. "Disease": dx,
  4237. "Structure": s,
  4238. "Mediator": med,
  4239. "Direct(c')": cprime,
  4240. "Indirect(a*b)": indirect,
  4241. "PropMediated": prop,
  4242. "CI_low": cil,
  4243. "CI_high": cih,
  4244. "p_val": pv,
  4245. "N": len(d)
  4246. })
  4247. # ---------------------------------------------------------------------
  4248. # Assemble dataframe + FDR by disease group
  4249. # ---------------------------------------------------------------------
  4250. results_df = pd.DataFrame(results)
  4251. if results_df.empty:
  4252. print("❌ No valid postmortem mediation results found.")
  4253. else:
  4254. results_df["p_FDR"] = np.nan
  4255. for dx in disease_groups:
  4256. sub = results_df[results_df["Disease"] == dx]
  4257. if sub.empty:
  4258. continue
  4259. reject, p_corr = pg.multicomp(sub["p_val"], method="fdr_bh")
  4260. results_df.loc[sub.index, "p_FDR"] = p_corr
  4261. # significance annotation
  4262. results_df["Sig(FDR)"] = results_df["p_FDR"].apply(
  4263. lambda p: "***" if p < 0.001 else
  4264. "**" if p < 0.01 else
  4265. "*" if p < 0.05 else ""
  4266. )
  4267. print("\n✅ Postmortem mediation results (FDR corrected within each group):\n")
  4268. print(results_df.round(4))
  4269. # %%
  4270. ################################################################################
  4271. # Postmortem Mediation Analysis + DOCX Supplementary Table
  4272. ################################################################################
  4273. import numpy as np
  4274. import pandas as pd
  4275. import warnings
  4276. warnings.filterwarnings("ignore", category=RuntimeWarning)
  4277. from statsmodels.formula.api import ols
  4278. import pingouin as pg
  4279. from docx import Document
  4280. from docx.enum.table import WD_TABLE_ALIGNMENT
  4281. from docx.oxml import OxmlElement
  4282. from docx.oxml.ns import qn
  4283. np.seterr(divide="ignore", invalid="ignore")
  4284. # ============================================================================
  4285. # 1) MEDIATION ANALYSIS
  4286. # ============================================================================
  4287. df_use = df.copy()
  4288. # Clean group label and postmortem columns
  4289. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  4290. for c in df_use.columns:
  4291. if c.startswith("postmortem_"):
  4292. df_use[c] = pd.to_numeric(df_use[c], errors="coerce")
  4293. disease_groups = ["alzheimer's disease", "ftld-tdp",
  4294. "lewy body disease", "tauopathies"]
  4295. region_map = {
  4296. "hippocampus": "EC_CS_DG",
  4297. "amygdala": "Amyg",
  4298. "caudate": "CP",
  4299. "putamen": "CP",
  4300. "thalamus": "TS",
  4301. "pallidum": "GP"
  4302. }
  4303. covars = ["AgeatDeath", "Sex", "Education", "PMI"]
  4304. mediators = ["NeuronLoss", "Gliosis"]
  4305. # --------------------------- Monte Carlo helper -----------------------------
  4306. def montecarlo_indirect(df_in, path_col, med_col, vol_col, covars,
  4307. n_iter=5000, seed=42):
  4308. """
  4309. Return:
  4310. c_prime, p_direct,
  4311. indirect, p_MC,
  4312. prop_med, ci_low, ci_high
  4313. """
  4314. np.random.seed(seed)
  4315. # a-path
  4316. a_mod = ols(f"{med_col} ~ {path_col} + {' + '.join(covars)}", data=df_in).fit()
  4317. # b-path (direct + mediator)
  4318. b_mod = ols(f"{vol_col} ~ {path_col} + {med_col} + {' + '.join(covars)}",
  4319. data=df_in).fit()
  4320. # total effect
  4321. c_mod = ols(f"{vol_col} ~ {path_col} + {' + '.join(covars)}", data=df_in).fit()
  4322. a = a_mod.params.get(path_col, np.nan)
  4323. b = b_mod.params.get(med_col, np.nan)
  4324. c_prime = b_mod.params.get(path_col, np.nan)
  4325. p_direct = b_mod.pvalues.get(path_col, np.nan)
  4326. c_total = c_mod.params.get(path_col, np.nan)
  4327. # MC samples for a*b
  4328. a_se = a_mod.bse.get(path_col, np.nan)
  4329. b_se = b_mod.bse.get(med_col, np.nan)
  4330. if np.isnan(a) or np.isnan(b) or np.isnan(a_se) or np.isnan(b_se):
  4331. return [np.nan]*7
  4332. a_draws = np.random.normal(a, a_se, n_iter)
  4333. b_draws = np.random.normal(b, b_se, n_iter)
  4334. ab_samples = a_draws * b_draws
  4335. indirect = np.mean(ab_samples)
  4336. ci_low, ci_high = np.percentile(ab_samples, [2.5, 97.5])
  4337. # two-sided MC p-value
  4338. p_MC = 2 * min(np.mean(ab_samples < 0), np.mean(ab_samples > 0))
  4339. prop_med = (indirect / c_total) if (c_total not in [0, np.nan]) else np.nan
  4340. return c_prime, p_direct, indirect, p_MC, prop_med, ci_low, ci_high
  4341. # --------------------------- Run mediation -----------------------------
  4342. results = []
  4343. for dx in disease_groups:
  4344. gdf = df_use[df_use["NPDx1"] == dx].copy()
  4345. if gdf.empty:
  4346. continue
  4347. if dx == "alzheimer's disease":
  4348. suffix = "Tau"
  4349. elif dx == "ftld-tdp":
  4350. suffix = "TDP43"
  4351. elif dx == "lewy body disease":
  4352. suffix = "aSyn"
  4353. elif dx == "tauopathies":
  4354. suffix = "Tau"
  4355. for s, prefix in region_map.items():
  4356. path_col = f"{prefix}{suffix}"
  4357. vol_col = f"postmortem_{s}"
  4358. for med in mediators:
  4359. med_col = f"{prefix}{med}"
  4360. if not (path_col in gdf.columns and
  4361. med_col in gdf.columns and
  4362. vol_col in gdf.columns):
  4363. continue
  4364. cols = [path_col, med_col, vol_col] + covars
  4365. d = gdf[cols].apply(pd.to_numeric, errors="coerce").dropna()
  4366. if len(d) < 15:
  4367. continue
  4368. # z-score continuous variables
  4369. for c in cols:
  4370. if d[c].std(ddof=0) > 0:
  4371. d[c] = (d[c] - d[c].mean()) / d[c].std(ddof=0)
  4372. (c_prime, p_direct,
  4373. indirect, p_MC,
  4374. prop, cil, cih) = montecarlo_indirect(
  4375. d, path_col, med_col, vol_col, covars
  4376. )
  4377. if np.isnan(indirect):
  4378. continue
  4379. results.append({
  4380. "Disease": dx,
  4381. "Structure": s,
  4382. "Mediator": med,
  4383. "Direct(c')": c_prime,
  4384. "p_direct": p_direct,
  4385. "Indirect(a*b)": indirect,
  4386. "p_MC": p_MC,
  4387. "PropMediated": prop,
  4388. "CI_low": cil,
  4389. "CI_high": cih,
  4390. "N": len(d)
  4391. })
  4392. results_df = pd.DataFrame(results)
  4393. if results_df.empty:
  4394. raise RuntimeError("No valid mediation results computed.")
  4395. # --------------------------- FDR corrections -----------------------------
  4396. results_df["p_FDR_direct"] = np.nan
  4397. results_df["p_FDR_MC"] = np.nan
  4398. for dx in disease_groups:
  4399. sub = results_df[results_df["Disease"] == dx]
  4400. if sub.empty:
  4401. continue
  4402. # FDR for direct effects
  4403. rej_d, p_corr_d = pg.multicomp(sub["p_direct"], method="fdr_bh")
  4404. results_df.loc[sub.index, "p_FDR_direct"] = p_corr_d
  4405. # FDR for indirect (MC) effects
  4406. rej_i, p_corr_i = pg.multicomp(sub["p_MC"], method="fdr_bh")
  4407. results_df.loc[sub.index, "p_FDR_MC"] = p_corr_i
  4408. # significance codes
  4409. def sig_star(p):
  4410. if p < 0.001:
  4411. return "***"
  4412. elif p < 0.01:
  4413. return "**"
  4414. elif p < 0.05:
  4415. return "*"
  4416. else:
  4417. return ""
  4418. results_df["Sig_direct"] = results_df["p_FDR_direct"].apply(sig_star)
  4419. results_df["Sig_MC"] = results_df["p_FDR_MC"].apply(sig_star)
  4420. print("Mediation results (head):")
  4421. print(results_df.head())
  4422. # ============================================================================
  4423. # 2) BUILD DOCX — mediation_analyses.docx
  4424. # ============================================================================
  4425. group_display = {
  4426. "alzheimer's disease": "Alzheimer’s disease",
  4427. "lewy body disease": "Lewy body disease",
  4428. "ftld-tdp": "FTLD-TDP",
  4429. "tauopathies": "Tauopathies"
  4430. }
  4431. mediator_display = {
  4432. "Gliosis": "Gliosis",
  4433. "NeuronLoss": "Neuronal loss"
  4434. }
  4435. structures_order = [
  4436. "hippocampus", "amygdala", "caudate",
  4437. "putamen", "thalamus", "pallidum"
  4438. ]
  4439. # ---------- small helpers ----------
  4440. def sci(x):
  4441. try:
  4442. return f"{float(x):.2e}"
  4443. except Exception:
  4444. return ""
  4445. def add_bold(cell, text):
  4446. run = cell.paragraphs[0].add_run(text)
  4447. run.bold = True
  4448. def add_superscript(cell, text):
  4449. run = cell.paragraphs[0].add_run(text)
  4450. run.font.superscript = True
  4451. def remove_top_border(cell):
  4452. """
  4453. Remove ONLY the top border of a cell (to hide line
  4454. between Gliosis and Neuronal loss rows).
  4455. """
  4456. tc = cell._tc
  4457. tcPr = tc.get_or_add_tcPr()
  4458. tcBorders = tcPr.find(qn('w:tcBorders'))
  4459. if tcBorders is None:
  4460. tcBorders = OxmlElement('w:tcBorders')
  4461. tcPr.append(tcBorders)
  4462. top = tcBorders.find(qn('w:top'))
  4463. if top is None:
  4464. top = OxmlElement('w:top')
  4465. tcBorders.append(top)
  4466. top.set(qn('w:val'), 'nil') # no border
  4467. # ---------- create doc ----------
  4468. doc = Document()
  4469. doc.add_heading("Supplementary Table — Postmortem Mediation Analyses", level=1)
  4470. intro = (
  4471. "For each diagnostic group, Monte Carlo mediation analyses were performed "
  4472. "with regional pathology as the predictor, gliosis or neuronal loss as "
  4473. "mediators, and postmortem MRI volume as the outcome. Models adjust for "
  4474. "age at death, sex, education, and PMI. FDR correction was applied within "
  4475. "each diagnostic group separately for direct effects and indirect "
  4476. "mediation effects. Asterisks (*, **, ***) indicate FDR-corrected "
  4477. "significance levels."
  4478. )
  4479. doc.add_paragraph(intro)
  4480. doc.add_paragraph("\n")
  4481. for dx in results_df["Disease"].unique():
  4482. sub = results_df[results_df["Disease"] == dx].copy()
  4483. if sub.empty:
  4484. continue
  4485. doc.add_heading(group_display.get(dx, dx), level=2)
  4486. sub["Structure"] = pd.Categorical(sub["Structure"], structures_order)
  4487. sub = sub.sort_values(["Structure", "Mediator"])
  4488. # table columns
  4489. cols = [
  4490. "Structure", "Mediator",
  4491. "Direct(c')", "p_direct", "p_FDR_direct",
  4492. "Indirect(a*b)", "p_MC", "p_FDR_MC",
  4493. "PropMediated", "CI"
  4494. ]
  4495. headers = {
  4496. "Structure": "Structure",
  4497. "Mediator": "Mediator",
  4498. "Direct(c')": "Direct (c′)",
  4499. "p_direct": "p(c′)",
  4500. "p_FDR_direct": "p_FDR(c′)",
  4501. "Indirect(a*b)": "Indirect (a×b)",
  4502. "p_MC": "p(MC)",
  4503. "p_FDR_MC": "p_FDR(MC)",
  4504. "PropMediated": "Proportion mediated",
  4505. "CI": "95% CI [low, high]"
  4506. }
  4507. table = doc.add_table(rows=1, cols=len(cols))
  4508. table.style = "Table Grid"
  4509. table.alignment = WD_TABLE_ALIGNMENT.CENTER
  4510. hdr = table.rows[0].cells
  4511. for j, c in enumerate(cols):
  4512. add_bold(hdr[j], headers[c])
  4513. # fill rows: structures × mediators (Gliosis, NeuronLoss)
  4514. for s in structures_order:
  4515. s_sub = sub[sub["Structure"] == s]
  4516. if s_sub.empty:
  4517. continue
  4518. for i, med in enumerate(["Gliosis", "NeuronLoss"]):
  4519. rowdata = s_sub[s_sub["Mediator"] == med]
  4520. if rowdata.empty:
  4521. continue
  4522. r = rowdata.iloc[0]
  4523. cells = table.add_row().cells
  4524. # structure label only on first mediator row
  4525. if i == 0:
  4526. cells[0].text = s.capitalize()
  4527. else:
  4528. cells[0].text = ""
  4529. # remove top border for all cells in second mediator row
  4530. for c in cells:
  4531. remove_top_border(c)
  4532. cells[1].text = mediator_display[med]
  4533. # direct effect + p
  4534. cells[2].text = sci(r["Direct(c')"])
  4535. cells[3].text = sci(r["p_direct"])
  4536. cells[4].text = sci(r["p_FDR_direct"])
  4537. if r["Sig_direct"]:
  4538. add_superscript(cells[4], r["Sig_direct"])
  4539. # indirect effect + p(MC)
  4540. cells[5].text = sci(r["Indirect(a*b)"])
  4541. cells[6].text = sci(r["p_MC"])
  4542. cells[7].text = sci(r["p_FDR_MC"])
  4543. if r["Sig_MC"]:
  4544. add_superscript(cells[7], r["Sig_MC"])
  4545. # proportion mediated + CI
  4546. cells[8].text = sci(r["PropMediated"])
  4547. cells[9].text = f"[{sci(r['CI_low'])}, {sci(r['CI_high'])}]"
  4548. doc.add_paragraph("\n")
  4549. out_name = "mediation_analyses.docx"
  4550. doc.save(out_name)
  4551. print(f"\nSaved Word file: {out_name}")
  4552. # %%
  4553. ##########################################################################################
  4554. # Postmortem Mediation
  4555. ##########################################################################################
  4556. import pandas as pd, numpy as np, seaborn as sns, matplotlib.pyplot as plt, pingouin as pg
  4557. from statsmodels.formula.api import ols
  4558. import warnings
  4559. warnings.filterwarnings("ignore", category=RuntimeWarning)
  4560. np.seterr(divide='ignore', invalid='ignore')
  4561. # ---------------------------------------------------------------------
  4562. # Use your already-cleaned dataframe
  4563. # ---------------------------------------------------------------------
  4564. df_use = df.copy() # <-- ADAPTED (NO re-cleaning)
  4565. # Ensure numeric postmortem volumes (safe cast only)
  4566. for c in df_use.columns:
  4567. if c.startswith("postmortem_"):
  4568. df_use[c] = pd.to_numeric(df_use[c], errors="coerce")
  4569. # ---------------------------------------------------------------------
  4570. # Settings
  4571. # ---------------------------------------------------------------------
  4572. disease_groups = ["alzheimer's disease", "ftld-tdp", "lewy body disease", "tauopathies"]
  4573. region_map = {
  4574. "Hippocampus": "EC_CS_DG",
  4575. "Amygdala": "Amyg",
  4576. "Caudate": "CP",
  4577. "Putamen": "CP",
  4578. "Thalamus": "TS",
  4579. "Pallidum": "GP"
  4580. }
  4581. covars = ["AgeatDeath", "Sex", "Education", "PMI"]
  4582. mediators = ["Gliosis", "NeuronLoss"]
  4583. # Mapping from disease → pathology suffix
  4584. path_suffix_map = {
  4585. "alzheimer's disease": "Tau",
  4586. "ftld-tdp": "TDP43",
  4587. "lewy body disease": "aSyn",
  4588. "tauopathies": "Tau"
  4589. }
  4590. # ---------------------------------------------------------------------
  4591. # Monte Carlo bootstrap indirect effect
  4592. # ---------------------------------------------------------------------
  4593. def montecarlo_indirect(df_mc, path_col, med_col, vol_col, covars, n_iter=5000, seed=42):
  4594. np.random.seed(seed)
  4595. try:
  4596. a_model = ols(f"{med_col} ~ {path_col} + {' + '.join(covars)}", data=df_mc).fit()
  4597. b_model = ols(f"{vol_col} ~ {path_col} + {med_col} + {' + '.join(covars)}", data=df_mc).fit()
  4598. c_model = ols(f"{vol_col} ~ {path_col} + {' + '.join(covars)}", data=df_mc).fit()
  4599. a = a_model.params.get(path_col, np.nan)
  4600. b = b_model.params.get(med_col, np.nan)
  4601. c_prime = b_model.params.get(path_col, np.nan)
  4602. c_total = c_model.params.get(path_col, np.nan)
  4603. a_draws = np.random.normal(a, a_model.bse.get(path_col, np.nan), n_iter)
  4604. b_draws = np.random.normal(b, b_model.bse.get(med_col, np.nan), n_iter)
  4605. ab_samples = a_draws * b_draws
  4606. indirect = np.mean(ab_samples)
  4607. ci_low, ci_high = np.percentile(ab_samples, [2.5, 97.5])
  4608. p_val = 2 * min(np.mean(ab_samples < 0), np.mean(ab_samples > 0))
  4609. prop = (indirect / c_total) if c_total not in [0, np.nan] else np.nan
  4610. return c_prime, indirect, prop, ci_low, ci_high, p_val
  4611. except:
  4612. return [np.nan] * 6
  4613. # ---------------------------------------------------------------------
  4614. # Run mediation analysis
  4615. # ---------------------------------------------------------------------
  4616. results = []
  4617. for dx in disease_groups:
  4618. gdf = df_use[df_use["NPDx1"] == dx].copy()
  4619. if gdf.empty:
  4620. continue
  4621. suffix = path_suffix_map[dx]
  4622. for s, prefix in region_map.items():
  4623. for med in mediators:
  4624. path_col = f"{prefix}{suffix}"
  4625. med_col = f"{prefix}{med}"
  4626. vol_col = f"postmortem_{s.lower()}"
  4627. if not all(c in gdf.columns for c in [path_col, med_col, vol_col]):
  4628. continue
  4629. cols = [path_col, med_col, vol_col] + covars
  4630. d = gdf[cols].apply(pd.to_numeric, errors="coerce").dropna()
  4631. if len(d) < 15:
  4632. continue
  4633. # Z-score numeric values
  4634. for c in cols:
  4635. if d[c].std(ddof=0) > 0:
  4636. d[c] = (d[c] - d[c].mean()) / d[c].std(ddof=0)
  4637. cprime, indirect, prop, cil, cih, pv = montecarlo_indirect(
  4638. d, path_col, med_col, vol_col, covars
  4639. )
  4640. if np.isnan(indirect):
  4641. continue
  4642. results.append({
  4643. "Disease": dx, "Structure": s, "Mediator": med,
  4644. "Direct(c')": cprime, "Indirect(a*b)": indirect,
  4645. "PropMediated": prop, "CI_low": cil, "CI_high": cih,
  4646. "p_val": pv, "N": len(d)
  4647. })
  4648. results_df = pd.DataFrame(results)
  4649. if results_df.empty:
  4650. raise SystemExit("❌ No valid postmortem mediation results found.")
  4651. # ---------------------------------------------------------------------
  4652. # FDR correction within each disease group
  4653. # ---------------------------------------------------------------------
  4654. results_df["p_FDR"] = np.nan
  4655. for dx in disease_groups:
  4656. sub = results_df[results_df["Disease"] == dx]
  4657. if sub.empty:
  4658. continue
  4659. _, p_corr = pg.multicomp(sub["p_val"], method="fdr_bh")
  4660. results_df.loc[sub.index, "p_FDR"] = p_corr
  4661. results_df["Sig(FDR)"] = results_df["p_FDR"].apply(
  4662. lambda p: "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
  4663. )
  4664. # ---------------------------------------------------------------------
  4665. # Publication-Ready Figure
  4666. # ---------------------------------------------------------------------
  4667. sns.set(style="white", context="talk")
  4668. effect_palette = {"Direct(c')": "#bca0dc", "Indirect(a*b)": "#a3d9a5"}
  4669. pretty_names = {
  4670. "alzheimer's disease": "Alzheimer’s Disease",
  4671. "ftld-tdp": "FTLD-TDP",
  4672. "lewy body disease": "Lewy Body Disease",
  4673. "tauopathies": "Tauopathies"
  4674. }
  4675. fig, axes = plt.subplots(2, 4, figsize=(20, 7.5),
  4676. sharey=True,
  4677. gridspec_kw={'hspace': 0.35, 'wspace': 0.10})
  4678. for row_i, med in enumerate(mediators):
  4679. for col_i, dx in enumerate(disease_groups):
  4680. ax = axes[row_i, col_i]
  4681. sub = results_df[(results_df["Mediator"] == med) &
  4682. (results_df["Disease"] == dx)]
  4683. if sub.empty:
  4684. ax.axis("off")
  4685. continue
  4686. plot_df = sub.melt(
  4687. id_vars=["Structure", "p_FDR", "Sig(FDR)"],
  4688. value_vars=["Direct(c')", "Indirect(a*b)"],
  4689. var_name="EffectType",
  4690. value_name="EffectSize"
  4691. )
  4692. # shorter x-labels
  4693. plot_df["Structure"] = plot_df["Structure"].replace({"Hippocampus": "Hippo"})
  4694. sns.barplot(
  4695. data=plot_df, x="Structure", y="EffectSize",
  4696. hue="EffectType", hue_order=["Direct(c')", "Indirect(a*b)"],
  4697. palette=effect_palette, dodge=True, edgecolor=None,
  4698. errorbar=None, ax=ax
  4699. )
  4700. ax.axhline(0, color="black", lw=0.6)
  4701. ax.tick_params(axis="x", rotation=0, labelsize=9)
  4702. ax.set_xlabel("")
  4703. ax.set_ylim(-0.65, 0.35)
  4704. if row_i == 0:
  4705. ax.set_title(pretty_names[dx], fontsize=14, weight="semibold")
  4706. # significance stars
  4707. ylim = ax.get_ylim()
  4708. for patch, (_, row) in zip(ax.patches, plot_df.iterrows()):
  4709. star = row["Sig(FDR)"]
  4710. if star:
  4711. height = patch.get_height()
  4712. y_pos = np.clip(height + (0.015 if height >= 0 else -0.015),
  4713. ylim[0] + 0.01, ylim[1] - 0.01)
  4714. ax.text(patch.get_x() + patch.get_width()/2, y_pos,
  4715. star, ha="center",
  4716. va="bottom" if height >= 0 else "top",
  4717. fontsize=11, weight="bold")
  4718. if ax.get_legend():
  4719. ax.get_legend().remove()
  4720. # Shared x/y labels
  4721. fig.text(0.5, 0.03, "Structure", ha="center", fontsize=15, weight="semibold")
  4722. fig.text(0.08, 0.5, "β Effect", va="center", ha="center",
  4723. rotation=90, fontsize=15, weight="semibold")
  4724. # Row titles
  4725. fig.text(0.5, 0.94, "Gliosis mediated effects", ha="center", fontsize=13)
  4726. fig.text(0.5, 0.46, "Neuron loss mediated effects", ha="center", fontsize=13)
  4727. # Legend
  4728. handles, labels = axes[0, 0].get_legend_handles_labels()
  4729. label_map = {"Direct(c')": "Direct", "Indirect(a*b)": "Indirect"}
  4730. labels = [label_map.get(l, l) for l in labels]
  4731. fig.legend(handles, labels, title="Effect", loc="lower center",
  4732. bbox_to_anchor=(0.5, -0.05), ncol=2,
  4733. frameon=False, fontsize=11, title_fontsize=12)
  4734. plt.suptitle("Postmortem Mediation: direct vs indirect effects", fontsize=16, y=0.99)
  4735. plt.tight_layout(rect=[0.05, 0.10, 1, 0.92])
  4736. print("✅ Figure saved as 'postmortem_mediation_final.png'")
  4737. plt.savefig("postmortem_mediation_final.png", dpi=600, bbox_inches="tight")
  4738. plt.show()
  4739. # %%
  4740. # %%
  4741. ######### Antemortem box plots
  4742. # ============================================================
  4743. # PREP
  4744. # ============================================================
  4745. df_use = merged_df.copy()
  4746. df_use["NPDx1"] = df_use["NPDx1"].astype(str).str.strip().str.lower()
  4747. order = ["alzheimer's disease", "lewy body disease", "ftld-tdp", "tauopathies"]
  4748. # Pretty x-labels
  4749. x_labels = [
  4750. "AD",
  4751. "LBD",
  4752. "FTLD-TDP",
  4753. "FTLD-Tau"
  4754. ]
  4755. covars = ["AgeatDeath", "Sex", "Education", "AMI"]
  4756. palette = ["#3366CC", "#DC3912", "#109618", "#FF9900"]
  4757. sns.set(style="whitegrid", context="talk", font_scale=1.2)
  4758. # ============================================================
  4759. # 7 REGIONS ONLY
  4760. # ============================================================
  4761. regions = [
  4762. "hippocampus",
  4763. "amygdala",
  4764. "accumbens_area",
  4765. "thalamus",
  4766. "caudate",
  4767. "putamen",
  4768. "pallidum"
  4769. ]
  4770. regions = [r for r in regions if f"antemortem_avg_{r}" in df_use.columns]
  4771. if "antemortem_icv" not in df_use.columns:
  4772. raise ValueError("antemortem_icv column not found")
  4773. print("Using regions:", regions)
  4774. # ============================================================
  4775. # NORMALIZE BY ANTEMORTEM ICV
  4776. # ============================================================
  4777. for r in regions:
  4778. avg_col = f"antemortem_avg_{r}"
  4779. norm_col = f"{r}_norm"
  4780. df_use[norm_col] = df_use[avg_col] / df_use["antemortem_icv"]
  4781. # ============================================================
  4782. # PAIRWISE LRTs
  4783. # ============================================================
  4784. pairwise_results = []
  4785. for r in regions:
  4786. ycol = f"{r}_norm"
  4787. for g1, g2 in combinations(order, 2):
  4788. d = df_use[df_use["NPDx1"].isin([g1, g2])].copy()
  4789. covars_here = [c for c in covars if c in d.columns]
  4790. d = d[["NPDx1", ycol] + covars_here].dropna()
  4791. if len(d) < 10:
  4792. continue
  4793. reduced = ols(f"{ycol} ~ " + " + ".join(covars_here), data=d).fit()
  4794. full = ols(f"{ycol} ~ C(NPDx1) + " + " + ".join(covars_here), data=d).fit()
  4795. lr = 2 * (full.llf - reduced.llf)
  4796. df_diff = full.df_model - reduced.df_model
  4797. p = chi2.sf(lr, df_diff)
  4798. pairwise_results.append({
  4799. "Region": r,
  4800. "Group1": g1,
  4801. "Group2": g2,
  4802. "p_raw": p
  4803. })
  4804. pairwise_df = pd.DataFrame(pairwise_results)
  4805. if not pairwise_df.empty:
  4806. reject, p_corr = pg.multicomp(pairwise_df["p_raw"], method="fdr_bh")
  4807. pairwise_df["p_FDR"] = p_corr
  4808. pairwise_df["Sig"] = pairwise_df["p_FDR"].apply(
  4809. lambda p: "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
  4810. )
  4811. else:
  4812. pairwise_df = pd.DataFrame(columns=["Region", "Group1", "Group2", "p_raw", "p_FDR", "Sig"])
  4813. print(pairwise_df)
  4814. # ============================================================
  4815. # PLOTTING
  4816. # ============================================================
  4817. fig = plt.figure(figsize=(26, 18))
  4818. # TOP PANEL (3)
  4819. gsA = fig.add_gridspec(
  4820. 1, 3, left=0.05, right=0.97,
  4821. top=0.92, bottom=0.56, wspace=0.33
  4822. )
  4823. axesA = [fig.add_subplot(gsA[0, k]) for k in range(3)]
  4824. # BOTTOM PANEL (4)
  4825. gsB = fig.add_gridspec(
  4826. 1, 4, left=0.05, right=0.97,
  4827. top=0.50, bottom=0.12, wspace=0.30
  4828. )
  4829. axesB = [fig.add_subplot(gsB[0, k]) for k in range(4)]
  4830. axes = axesA + axesB
  4831. def plot_struct(ax, r):
  4832. ycol = f"{r}_norm"
  4833. d = df_use[["NPDx1", ycol]].dropna()
  4834. for idx, g in enumerate(order):
  4835. vals = d.loc[d["NPDx1"] == g, ycol]
  4836. tmp = pd.DataFrame({"group": [g] * len(vals), "y": vals})
  4837. sns.boxplot(
  4838. data=tmp, x="group", y="y",
  4839. color=palette[idx], ax=ax,
  4840. width=0.55, fliersize=0,
  4841. linewidth=1.3, boxprops=dict(alpha=0.72)
  4842. )
  4843. sns.stripplot(
  4844. data=tmp, x="group", y="y",
  4845. color="black", size=4, alpha=0.55,
  4846. ax=ax, jitter=0.15
  4847. )
  4848. ax.set_xlabel("")
  4849. ax.set_ylabel("")
  4850. ax.set_xticklabels(x_labels, fontsize=16, fontweight="bold")
  4851. ymin, ymax = d[ycol].min(), d[ycol].max()
  4852. yr = ymax - ymin if ymax > ymin else 1.0
  4853. ax.set_ylim(ymin - 0.06 * yr, ymax + 0.45 * yr)
  4854. ax.set_title(r.replace("_", " ").capitalize(), fontsize=18, fontweight="bold")
  4855. ax.grid(axis="y", linestyle=":", alpha=0.45)
  4856. pairs = pairwise_df[pairwise_df["Region"] == r]
  4857. y_offset = 0.020 * yr
  4858. y_pos = ymax + 0.10 * yr
  4859. for _, row in pairs.iterrows():
  4860. if row["Sig"]:
  4861. g1, g2 = row["Group1"], row["Group2"]
  4862. x1 = order.index(g1)
  4863. x2 = order.index(g2)
  4864. ax.plot([x1, x1, x2, x2],
  4865. [y_pos, y_pos + y_offset, y_pos + y_offset, y_pos],
  4866. lw=1.35, color="black")
  4867. ax.text((x1 + x2) / 2, y_pos + y_offset * 0.8,
  4868. row["Sig"], ha="center",
  4869. fontsize=14, fontweight="bold")
  4870. y_pos += y_offset * 1.9
  4871. for ax, r in zip(axes, regions):
  4872. plot_struct(ax, r)
  4873. fig.text(0.0001, 0.55, "Normalized volume",
  4874. va="center", rotation="vertical",
  4875. fontsize=25, fontweight="bold")
  4876. fig.text(0.50, 0.06, "Disease groups",
  4877. ha="center", fontsize=25, fontweight="bold")
  4878. plt.subplots_adjust(top=0.93)
  4879. fig.suptitle(
  4880. "Antemortem subcortical and limbic volumes differentiates neuropathological groups",
  4881. fontsize=26, fontweight="bold"
  4882. )
  4883. plt.tight_layout()
  4884. plt.savefig("antemortem_avg_subcortical_volumes_normalized_by_icv.png",
  4885. dpi=600, bbox_inches="tight")
  4886. plt.show()
  4887. # %%
  4888. ############################
  4889. ############################
  4890. """
  4891. For the 5 groups, the groups to be useed are:
  4892. order = [
  4893. "alzheimer's disease",
  4894. "lewy body disease",
  4895. "ftld-tdp",
  4896. "3R-tau",
  4897. "4R-tau"
  4898. ]
  4899. """

subcortical_analyses.ipynb at commit 6abfd1d, no license · at the source

Overview

Authors: Pulkit Khandelwal1,2, Michael Tran Duong1, Lisa M Levorse1, Winifred Trotman1, Alejandra Bahena1, Sydney A Lim1, Amanda E Denning1, Eunice Chung1, Christopher A Olm1, Hamsanandini Radhakrishnan1, Ranjit Ittyerah1, Karthik Prabhakaran1, Gabor Mizsei1, Theresa Schuck1, Sheina Emrani1, Joaquin A Vizcarra1, John Robinson1, Daniel T Ohm1, Jeffrey S Phillips1, Jesse Cohen3
and 11 other authorsLaura E M Wisse4, John A Detre1, Ilya M Nasrallah1, Christopher A Brown1, Sandhitsu R Das1, Edward B Lee1, M Dylan Tisdall1, David J Irwin1, Corey T McMillan1, David A Wolk1, Paul A Yushkevich1
  1. University of Pennsylvania, Philadelphia, Pennsylvania, USA
  2. Athinoula A. Martinos Center for Biomedical Imaging, Massachusetts General Hospital and Harvard Medical School, Charlestown, Massachusetts, USA
  3. University of Florida, Jacksonville, Florida, USA
  4. Lund University, Lund, Sweden
Institutions: Harvard University (United States); Massachusetts General Hospital (United States); Athinoula A. Martinos Center for Biomedical Imaging (United States); University of Pennsylvania (United States); University of Florida (United States); Lund University (Sweden)
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association, volume 22, issue 7, article e71649
Dates: received 7 January 2026; accepted 6 June 2026; published online 8 July 2026; in print July 2026
Type: Research article · Language: English
License: CC BY-NC
Identifiers: DOI 10.1002/alz.71649 · PMID 42418450 · PMCID PMC13344898 · OpenAlex W7167693549
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), histology / microscopy (modality), human (organism), other condition (population), Alzheimer's / dementia (population), Parkinson's (population), cellular / molecular (subfield)
Methods: Statistics, Smoothing, state filtering, decompositions, Connectivity, Preprocessing
Keywords: ADRD, alzheimer's disease, alzheimer's disease and related dementias, copathologies, entorhinal cortex, ex vivo magnetic resonance imaging (MRI), frontotemporal lobar degeneration with transactive response DNA binding protein 43 (FTLD‐TDP), FTLD‐tau, lewy body dementia, neuroimaging, neuropathologies, postmortem imaging, subcortical structures, synuclein, tau, TDP‐43
MeSH: Alzheimer Disease*, Brain*, Cerebral Cortical Thinning*, Frontotemporal Lobar Degeneration*, Lewy Body Disease*, Limbic System*, Magnetic Resonance Imaging*, Neurodegenerative Diseases*, Aged, Aged, 80 and over, alpha-Synuclein, Atrophy, DNA-Binding Proteins, Female, Humans, Male, Middle Aged, tau Proteins (* major topic)
Topic: Dementia and Cognitive Impairment Research (Psychiatry and Mental health, Medicine), according to OpenAlex
Funding: NIH HHS (P01 AG017586, F30 AG074524, P30 AG072979, U19 AG062418, P01 AG066597, R01 NS109260, RF1 AG056014, P01 AG084497, R01 AG054519, R01 AG069474); National Institutes of Health (R01 AG054519, P01 AG084497, U19 AG062418, P01 AG017586, P01 AG066597, R01 NS109260, F30 AG074524, P30 AG072979, R01 AG069474, RF1 AG056014)
Citations: not cited yet (Europe PMC); 74 references in the paper

Abstract

INTRODUCTION: The impact of different neuropathologies on deep brain structures remains to be understood. We examine subcortical and limbic volumetry in neurodegenerative diseases involving phosphorylated tau (p‐tau), α‐synuclein, and transactive response DNA binding protein 43 (TDP‐43).

METHODS: We acquired neuropathological measures and brain segmentations from postmortem analysis of 132 donors with Alzheimer's disease (AD), Lewy body disease (LBD), frontotemporal lobar degeneration with TDP‐43 (FTLD‐TDP), and FTLD‐tau.

RESULTS: LBD had the least subcortical, limbic, and cortical atrophy compared to AD, FTLD‐TDP, and FTLD‐tau. In donors with both AD and LBD pathologies, primary LBD was associated with less atrophy than primary AD. While AD had cortico‐subcortical and cortico‐limbic morphometric associations, LBD had more limited parieto‐occipital cortico‐limbic associations. FTLD‐TDP had cortico‐subcortical while FTLD‐tau had cortico‐subcortical and cortico‐limbic associations. In AD and FTLD‐tau, hippocampal volumes correlated with p‐tau burden, neuron loss, and gliosis. In LBD, thalamic α‐synuclein severity was associated with subcortical/limbic volumes.

DISCUSSION: Postmortem neuroimaging reveals disease‐ and region‐specific structure–pathology relationships.

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

Repositories

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

Pulkit-Khandelwal/purple-mri

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 671f35b6d8eb41e3f75c4ff3b16cedc1986a9b24, 10 September 2026
Languages: Shell (39), Python (29), C++ (1), Jupyter (1)
Size: 178 files, 70 scripts
Software Heritage: not archived
Found in: the text, “Image analysis”
Holds: README, environment (pyproject.toml, docs/requirements.txt, pkg_src/pyproject.toml, docker/nighres_docker/Dockerfile, docker/segmentation_docker/Dockerfile, docker/segmentation_docker/requirements.txt, docs/source/_static/singularity.png), documentation, 1 notebook
Not found: license file, CITATION.cff, tests, continuous integration
Tools: NumPy (24 files), FreeSurfer (21 files), PyTorch (14 files), SciPy (13 files), SimpleITK (12 files), nnU-Net (11 files), OpenCV (9 files), pandas (7 files), NiBabel (6 files), ANTs (2 files), Matplotlib (2 files), Pillow (2 files), FSL (1 file), Nighres (1 file), Pingouin (1 file), Plotly (1 file), scikit-image (1 file), scikit-learn (1 file), seaborn (1 file), statannotations (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
71 files

Pulkit-Khandelwal/postmortem-subcortical-limbic-pathologies

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 6abfd1d98fa06009578e0e292a72996ba3e46a58, 5 May 2026
Languages: Jupyter (2)
Size: 3 files, 2 scripts
Software Heritage: not archived
Found in: the text, “Statistical analysis”
Holds: README, 2 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (2 files), NumPy (2 files), pandas (2 files), Pingouin (2 files), SciPy (2 files), seaborn (2 files), statsmodels (2 files), NiBabel (1 file), Pillow (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
3 files

Tracing map

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

What the map holds:

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

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 31 authors, 16 keywords, 18 MeSH terms, 2 funders, 71 references.

Cite

This paper

Khandelwal, P., Duong, M. T., Levorse, L. M., Trotman, W., Bahena, A., Lim, S. A., Denning, A. E., Chung, E., Olm, C. A., Radhakrishnan, H., Ittyerah, R., Prabhakaran, K., Mizsei, G., Schuck, T., Emrani, S., Vizcarra, J. A., Robinson, J., Ohm, D. T., Phillips, J. S., . . . Yushkevich, P. A. (2026). Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies. Alzheimer's & dementia : the journal of the Alzheimer's Association, 22(7), e71649. https://doi.org/10.1002/alz.71649

BibTeX

@article{khandelwal2026postmortem,
author = {Khandelwal, Pulkit and Duong, Michael Tran and Levorse, Lisa M and Trotman, Winifred and Bahena, Alejandra and Lim, Sydney A and Denning, Amanda E and Chung, Eunice and Olm, Christopher A and Radhakrishnan, Hamsanandini and Ittyerah, Ranjit and Prabhakaran, Karthik and Mizsei, Gabor and Schuck, Theresa and Emrani, Sheina and Vizcarra, Joaquin A and Robinson, John and Ohm, Daniel T and Phillips, Jeffrey S and Cohen, Jesse and Wisse, Laura E M and Detre, John A and Nasrallah, Ilya M and Brown, Christopher A and Das, Sandhitsu R and Lee, Edward B and Tisdall, M Dylan and Irwin, David J and McMillan, Corey T and Wolk, David A and Yushkevich, Paul A},
title = {{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},
year = {2026},
month = jul,
volume = {22},
number = {7},
pages = {e71649},
publisher = {Wiley},
issn = {1552-5260},
doi = {10.1002/alz.71649},
url = {https://doi.org/10.1002/alz.71649},
pmid = {42418450},
pmcid = {PMC13344898}
}

RIS

TY - JOUR
AU - Khandelwal, Pulkit
AU - Duong, Michael Tran
AU - Levorse, Lisa M
AU - Trotman, Winifred
AU - Bahena, Alejandra
AU - Lim, Sydney A
AU - Denning, Amanda E
AU - Chung, Eunice
AU - Olm, Christopher A
AU - Radhakrishnan, Hamsanandini
AU - Ittyerah, Ranjit
AU - Prabhakaran, Karthik
AU - Mizsei, Gabor
AU - Schuck, Theresa
AU - Emrani, Sheina
AU - Vizcarra, Joaquin A
AU - Robinson, John
AU - Ohm, Daniel T
AU - Phillips, Jeffrey S
AU - Cohen, Jesse
AU - Wisse, Laura E M
AU - Detre, John A
AU - Nasrallah, Ilya M
AU - Brown, Christopher A
AU - Das, Sandhitsu R
AU - Lee, Edward B
AU - Tisdall, M Dylan
AU - Irwin, David J
AU - McMillan, Corey T
AU - Wolk, David A
AU - Yushkevich, Paul A
TI - Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies
T2 - Alzheimer's & dementia : the journal of the Alzheimer's Association
J2 - Alzheimers Dement
PY - 2026
DA - 2026/07/01
VL - 22
IS - 7
SP - e71649
SN - 1552-5260
PB - Wiley
DO - 10.1002/alz.71649
UR - https://doi.org/10.1002/alz.71649
LA - en
ER -

CSL-JSON

{
"id": "10.1002/alz.71649",
"type": "article-journal",
"title": "Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies",
"container-title": "Alzheimer's & dementia : the journal of the Alzheimer's Association",
"author": [
{
"family": "Khandelwal",
"given": "Pulkit"
},
{
"family": "Duong",
"given": "Michael Tran"
},
{
"family": "Levorse",
"given": "Lisa M"
},
{
"family": "Trotman",
"given": "Winifred"
},
{
"family": "Bahena",
"given": "Alejandra"
},
{
"family": "Lim",
"given": "Sydney A"
},
{
"family": "Denning",
"given": "Amanda E"
},
{
"family": "Chung",
"given": "Eunice"
},
{
"family": "Olm",
"given": "Christopher A"
},
{
"family": "Radhakrishnan",
"given": "Hamsanandini"
},
{
"family": "Ittyerah",
"given": "Ranjit"
},
{
"family": "Prabhakaran",
"given": "Karthik"
},
{
"family": "Mizsei",
"given": "Gabor"
},
{
"family": "Schuck",
"given": "Theresa"
},
{
"family": "Emrani",
"given": "Sheina"
},
{
"family": "Vizcarra",
"given": "Joaquin A"
},
{
"family": "Robinson",
"given": "John"
},
{
"family": "Ohm",
"given": "Daniel T"
},
{
"family": "Phillips",
"given": "Jeffrey S"
},
{
"family": "Cohen",
"given": "Jesse"
},
{
"family": "Wisse",
"given": "Laura E M"
},
{
"family": "Detre",
"given": "John A"
},
{
"family": "Nasrallah",
"given": "Ilya M"
},
{
"family": "Brown",
"given": "Christopher A"
},
{
"family": "Das",
"given": "Sandhitsu R"
},
{
"family": "Lee",
"given": "Edward B"
},
{
"family": "Tisdall",
"given": "M Dylan"
},
{
"family": "Irwin",
"given": "David J"
},
{
"family": "McMillan",
"given": "Corey T"
},
{
"family": "Wolk",
"given": "David A"
},
{
"family": "Yushkevich",
"given": "Paul A"
}
],
"container-title-short": "Alzheimers Dement",
"volume": "22",
"issue": "7",
"page": "e71649",
"DOI": "10.1002/alz.71649",
"PMID": "42418450",
"PMCID": "PMC13344898",
"ISSN": "1552-5260",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/alz.71649",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
1
]
]
}
}

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

Similar papers

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

[1] doi:10.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: statannotations, ANTs, FreeSurfer, 11 other tools, 1 reference
[2] 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: statannotations, ANTs, FreeSurfer, 11 other tools, 1 reference
[3] doi:10.1038/s41598-026-55397-w [code]
Fast surface reconstruction of human brain MRI: benchmarking deep-learning based morphometry tools.
Journal: Scientific reports
In common: Nighres, SimpleITK, ANTs, 8 other tools, structural MRI / diffusion, 2 references
[4] doi:10.1002/hipo.70124 [code]
Association Between Anterior Hippocampal Gyrification and Episodic Memory Performance in Neurotypical Young Adults.
Journal: Hippocampus
In common: Nighres, nnU-Net, SimpleITK, 9 other tools, structural MRI / diffusion, 1 reference
[5] doi:10.1162/imag.a.1164 [code]
Bias and generalizability of brain age prediction models: A multi-cohort evaluation with anatomical and interpretability insights.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: ANTs, FreeSurfer, FSL, 11 other tools, Alzheimer's / dementia, structural MRI / diffusion
[6] doi:10.3389/frai.2026.1771088 [code]
Few-shot deployment of pretrained MRI transformers in brain imaging tasks.
Journal: Frontiers in artificial intelligence
In common: nnU-Net, SimpleITK, OpenCV, 10 other tools, structural MRI / diffusion, 1 reference
[7] doi:10.2463/mrms.mp.2024-0149 [code]
Image Distortion Correction for Diffusion MR Imaging Using a Transformer-based U-Net.
Journal: Magnetic resonance in medical sciences : MRMS : an official journal of Japan Society of Magnetic Resonance in Medicine
In common: nnU-Net, ANTs, FreeSurfer, 10 other tools, structural MRI / diffusion
[8] doi:10.1371/journal.pcbi.1014555 [code]
Body surface potential driven personalisation of electrophysiological digital twins in hypertrophic cardiomyopathy.
Journal: PLoS computational biology
In common: nnU-Net, SimpleITK, ANTs, 10 other tools, structural MRI / diffusion
[9] doi:10.3389/fnins.2026.1870124 [code]
An end-to-end pipeline for automated fetal brain segmentation and biometry from 3D SSFP MRI.
Journal: Frontiers in neuroscience
In common: nnU-Net, SimpleITK, FSL, 10 other tools, structural MRI / diffusion
[10] doi:10.1016/j.crmeth.2026.101473 [code]
AmygdalaGo-BOLT for boundary-aware segmentation of the human amygdala.
Journal: Cell reports methods
In common: SimpleITK, ANTs, FreeSurfer, 9 other tools, structural MRI / diffusion

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.