OSCR

Medial temporal lobe tau-neurodegeneration mismatch from structural imaging and plasma biomarkers.

Code ↔ Paper

6 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 6 matches
  1. [1] § Materials and methods › Assigning clinical patients to ADNI-derived T-N phenotypes ↔ src/mismatch/AssignGroup.py, lines 8 to 137 · score 0.71 · cluster centroid, binarized residual, SD, ATM, Euclidean, threshold · Report a mismatch
  2. [2] § Materials and methods › T-N mismatch modelling ↔ src/mismatch/TNMismatchClustering copy.py, lines 424 to 543 · score 0.63 · hierarchical clustering, Ward, dendrogram, bisquare, SD, regression · Report a mismatch
  3. [3] § Materials and methods › T-N mismatch modelling ↔ src/mismatch/TNMismatchClustering.py, lines 465 to 584 · score 0.63 · hierarchical clustering, Ward, dendrogram, bisquare, SD, regression · Report a mismatch
  4. [4] § Materials and methods › Image acquisition and processing › MTL super-points parcellation ↔ src/mismatch/__main__.py, lines 17 to 130 · score 0.61 · spatially contiguous, super point, partitioning, template, graph, clustering · Report a mismatch
  5. [5] § Materials and methods › Image acquisition and processing › MTL super-points parcellation ↔ src/mismatch/MTLSuperPoints.py, lines 123 to 219 · score 0.54 · super point, PyMetis, partitioning, graph, adjacency, template · Report a mismatch
  6. [6] § Materials and methods › Assigning clinical patients to ADNI-derived T-N phenotypes ↔ src/mismatch/TNMismatchClustering copy.py, lines 424 to 543 · score 0.52 · binarized residual, SD, matched, threshold, cluster, regression · Report a mismatch

Paper

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

The paper is loaded when this pane is shown.

The authors' code

Python · 543 lines · 23 KB · MIT · 2 matches

  1. # tn_mismatch_clustering.py
  2. import pandas as pd
  3. import numpy as np
  4. import statsmodels.api as sm
  5. from sklearn.preprocessing import StandardScaler
  6. from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
  7. from scipy.stats import pearsonr
  8. from scipy.spatial.distance import pdist
  9. import matplotlib.pyplot as plt
  10. import seaborn as sns
  11. import argparse
  12. import os
  13. import nibabel as nib
  14. DKT_LABEL_MAP = {
  15. 1002: 'lh.caudalanteriorcingulate', 1003: 'lh.caudalmiddlefrontal', 1005: 'lh.cuneus',
  16. 1006: 'lh.entorhinal', 1007: 'lh.fusiform', 1008: 'lh.inferiorparietal', 1009: 'lh.inferiortemporal',
  17. 1010: 'lh.isthmuscingulate', 1011: 'lh.lateraloccipital', 1012: 'lh.lateralorbitofrontal',
  18. 1013: 'lh.lingual', 1014: 'lh.medialorbitofrontal', 1015: 'lh.middletemporal', 1016: 'lh.parahippocampal',
  19. 1017: 'lh.paracentral', 1018: 'lh.parsopercularis', 1019: 'lh.parsorbitalis', 1020: 'lh.parstriangularis',
  20. 1021: 'lh.pericalcarine', 1022: 'lh.postcentral', 1023: 'lh.posteriorcingulate', 1024: 'lh.precentral',
  21. 1025: 'lh.precuneus', 1026: 'lh.rostralanteriorcingulate', 1027: 'lh.rostralmiddlefrontal',
  22. 1028: 'lh.superiorfrontal', 1029: 'lh.superiorparietal', 1030: 'lh.superiortemporal',
  23. 1031: 'lh.supramarginal', 1034: 'lh.transversetemporal', 1035: 'lh.insula',
  24. 2002: 'rh.caudalanteriorcingulate', 2003: 'rh.caudalmiddlefrontal', 2005: 'rh.cuneus',
  25. 2006: 'rh.entorhinal', 2007: 'rh.fusiform', 2008: 'rh.inferiorparietal', 2009: 'rh.inferiortemporal',
  26. 2010: 'rh.isthmuscingulate', 2011: 'rh.lateraloccipital', 2012: 'rh.lateralorbitofrontal',
  27. 2013: 'rh.lingual', 2014: 'rh.medialorbitofrontal', 2015: 'rh.middletemporal',
  28. 2016: 'rh.parahippocampal', 2017: 'rh.paracentral', 2018: 'rh.parsopercularis',
  29. 2019: 'rh.parsorbitalis', 2020: 'rh.parstriangularis', 2021: 'rh.pericalcarine',
  30. 2022: 'rh.postcentral', 2023: 'rh.posteriorcingulate', 2024: 'rh.precentral',
  31. 2025: 'rh.precuneus', 2026: 'rh.rostralanteriorcingulate', 2027: 'rh.rostralmiddlefrontal',
  32. 2028: 'rh.superiorfrontal', 2029: 'rh.superiorparietal', 2030: 'rh.superiortemporal',
  33. 2031: 'rh.supramarginal', 2034: 'rh.transversetemporal', 2035: 'rh.insula'
  34. }
  35. BrainCOLOR_LABEL_MAP = {
  36. 100: 'Right.ACgG.anterior.cingulate.gyrus',
  37. 101: 'Left.ACgG.anterior.cingulate.gyrus',
  38. 102: 'Right.AIns.anterior.insula',
  39. 103: 'Left.AIns.anterior.insula',
  40. 104: 'Right.AOrG.anterior.orbital.gyrus',
  41. 105: 'Left.AOrG.anterior.orbital.gyrus',
  42. 106: 'Right.AnG.angular.gyrus',
  43. 107: 'Left.AnG.angular.gyrus',
  44. 108: 'Right.Calc.calcarine.cortex',
  45. 109: 'Left.Calc.calcarine.cortex',
  46. 112: 'Right.CO.central.operculum',
  47. 113: 'Left.CO.central.operculum',
  48. 114: 'Right.Cun.cuneus',
  49. 115: 'Left.Cun.cuneus',
  50. 116: 'Right.Ent.entorhinal.area',
  51. 117: 'Left.Ent.entorhinal.area',
  52. 118: 'Right.FO.frontal.operculum',
  53. 119: 'Left.FO.frontal.operculum',
  54. 120: 'Right.FRP.frontal.pole',
  55. 121: 'Left.FRP.frontal.pole',
  56. 122: 'Right.FuG.fusiform.gyrus',
  57. 123: 'Left.FuG.fusiform.gyrus',
  58. 124: 'Right.GRe.gyrus.rectus',
  59. 125: 'Left.GRe.gyrus.rectus',
  60. 128: 'Right.IOG.inferior.occipital.gyrus',
  61. 129: 'Left.IOG.inferior.occipital.gyrus',
  62. 132: 'Right.ITG.inferior.temporal.gyrus',
  63. 133: 'Left.ITG.inferior.temporal.gyrus',
  64. 134: 'Right.LiG.lingual.gyrus',
  65. 135: 'Left.LiG.lingual.gyrus',
  66. 136: 'Right.LOrG.lateral.orbital.gyrus',
  67. 137: 'Left.LOrG.lateral.orbital.gyrus',
  68. 138: 'Right.MCgG.middle.cingulate.gyrus',
  69. 139: 'Left.MCgG.middle.cingulate.gyrus',
  70. 140: 'Right.MFC.medial.frontal.cortex',
  71. 141: 'Left.MFC.medial.frontal.cortex',
  72. 142: 'Right.MFG.middle.frontal.gyrus',
  73. 143: 'Left.MFG.middle.frontal.gyrus',
  74. 144: 'Right.MOG.middle.occipital.gyrus',
  75. 145: 'Left.MOG.middle.occipital.gyrus',
  76. 146: 'Right.MOrG.medial.orbital.gyrus',
  77. 147: 'Left.MOrG.medial.orbital.gyrus',
  78. 148: 'Right.MPoG.postcentral.gyrus.medial.segment',
  79. 149: 'Left.MPoG.postcentral.gyrus.medial.segment',
  80. 150: 'Right.MPrG.precentral.gyrus.medial.segment',
  81. 151: 'Left.MPrG.precentral.gyrus.medial.segment',
  82. 152: 'Right.MSFG.superior.frontal.gyrus.medial.segment',
  83. 153: 'Left.MSFG.superior.frontal.gyrus.medial.segment',
  84. 154: 'Right.MTG.middle.temporal.gyrus',
  85. 155: 'Left.MTG.middle.temporal.gyrus',
  86. 156: 'Right.OCP.occipital.pole',
  87. 157: 'Left.OCP.occipital.pole',
  88. 160: 'Right.OFuG.occipital.fusiform.gyrus',
  89. 161: 'Left.OFuG.occipital.fusiform.gyrus',
  90. 162: 'Right.OpIFG.opercular.part.of.IFG',
  91. 163: 'Left.OpIFG.opercular.part.of.IFG',
  92. 164: 'Right.OrIFG.orbital.part.of.IFG',
  93. 165: 'Left.OrIFG.orbital.part.of.IFG',
  94. 166: 'Right.PCgG.posterior.cingulate.gyrus',
  95. 167: 'Left.PCgG.posterior.cingulate.gyrus',
  96. 168: 'Right.PCu.precuneus',
  97. 169: 'Left.PCu.precuneus',
  98. 170: 'Right.PHG.parahippocampal.gyrus',
  99. 171: 'Left.PHG.parahippocampal.gyrus',
  100. 172: 'Right.PIns.posterior.insula',
  101. 173: 'Left.PIns.posterior.insula',
  102. 174: 'Right.PO.parietal.operculum',
  103. 175: 'Left.PO.parietal.operculum',
  104. 176: 'Right.PoG.postcentral.gyrus',
  105. 177: 'Left.PoG.postcentral.gyrus',
  106. 178: 'Right.POrG.posterior.orbital.gyrus',
  107. 179: 'Left.POrG.posterior.orbital.gyrus',
  108. 180: 'Right.PP.planum.polare',
  109. 181: 'Left.PP.planum.polare',
  110. 182: 'Right.PrG.precentral.gyrus',
  111. 183: 'Left.PrG.precentral.gyrus',
  112. 184: 'Right.PT.planum.temporale',
  113. 185: 'Left.PT.planum.temporale',
  114. 186: 'Right.SCA.subcallosal.area',
  115. 187: 'Left.SCA.subcallosal.area',
  116. 190: 'Right.SFG.superior.frontal.gyrus',
  117. 191: 'Left.SFG.superior.frontal.gyrus',
  118. 192: 'Right.SMC.supplementary.motor.cortex',
  119. 193: 'Left.SMC.supplementary.motor.cortex',
  120. 194: 'Right.SMG.supramarginal.gyrus',
  121. 195: 'Left.SMG.supramarginal.gyrus',
  122. 196: 'Right.SOG.superior.occipital.gyrus',
  123. 197: 'Left.SOG.superior.occipital.gyrus',
  124. 198: 'Right.SPL.superior.parietal.lobule',
  125. 199: 'Left.SPL.superior.parietal.lobule',
  126. 200: 'Right.STG.superior.temporal.gyrus',
  127. 201: 'Left.STG.superior.temporal.gyrus',
  128. 202: 'Right.TMP.temporal.pole',
  129. 203: 'Left.TMP.temporal.pole',
  130. 204: 'Right.TrIFG.triangular.part.of.IFG',
  131. 205: 'Left.TrIFG.triangular.part.of.IFG',
  132. 206: 'Right.TTG.transverse.temporal.gyrus',
  133. 207: 'Left.TTG.transverse.temporal.gyrus',
  134. 31: 'Right.Amygdala', 32: 'Left.Amygdala', 47: 'Right.Hippocampus', 48: 'Left.Hippocampus',
  135. 35: 'Brain.Stem', 36: 'Right.Caudate', 37: 'Left.Caudate',
  136. 44: 'Right.Cerebral.White.Matter', 45: 'Left.Cerebral.White.Matter'
  137. }
  138. ATLAS_CONFIG = {
  139. "DKT": {
  140. "path": "/project/wolk/xyl/tn_mismatch/tn_mismatch/preprocess/tpl-ADNINormalAgingANTs_res-01_atlas-DKT_desc-31_dseg.nii.gz",
  141. "label_map": DKT_LABEL_MAP
  142. },
  143. "BrainCOLOR": {
  144. "path": "/project/wolk/xyl/tn_mismatch/tn_mismatch/preprocess/antsMalfLabeling.nii.gz",
  145. "label_map": BrainCOLOR_LABEL_MAP
  146. }
  147. }
  148. class TNMismatchClustering:
  149. def __init__(self, atlas=None, custom_atlas_path=None, custom_label_csv=None):
  150. self.atlas = atlas
  151. self.custom_atlas_path = custom_atlas_path
  152. self.custom_label_csv = custom_label_csv
  153. self.seg_path = None
  154. self.label_map = None
  155. self.allowed_labels = None
  156. self._load_atlas()
  157. def _load_atlas(self):
  158. if self.custom_atlas_path and self.custom_label_csv:
  159. self.seg_path = self.custom_atlas_path
  160. self.label_map = self.load_custom_label_csv(self.custom_label_csv)
  161. self.allowed_labels = set(self.label_map.keys())
  162. elif self.atlas:
  163. atlas_info = ATLAS_CONFIG.get(self.atlas)
  164. self.seg_path = atlas_info["path"]
  165. self.label_map = atlas_info["label_map"]
  166. self.allowed_labels = set(self.label_map.keys())
  167. def load_custom_label_csv(self, label_csv):
  168. df = pd.read_csv(label_csv)
  169. colnames = [c.lower() for c in df.columns]
  170. if "index" in colnames and "name" in colnames:
  171. index_col = df.columns[colnames.index("index")]
  172. name_col = df.columns[colnames.index("name")]
  173. elif "labelid" in colnames and "labelname" in colnames:
  174. index_col = df.columns[colnames.index("labelid")]
  175. name_col = df.columns[colnames.index("labelname")]
  176. else:
  177. raise ValueError("CSV must contain either 'index,name' or 'LabelID,LabelName' columns")
  178. return {int(row[index_col]): row[name_col] for _, row in df.iterrows()}
  179. def extract_matched_tau_thickness(self, df, independent, predict):
  180. tau_cols = [c for c in df.columns if c.endswith(f"_{independent}")]
  181. matched = []
  182. unmatched = []
  183. for c in tau_cols:
  184. base = c.replace(f"_{independent}", "")
  185. target = f"{base}_{predict}"
  186. if target in df.columns:
  187. matched.append((c, target))
  188. else:
  189. unmatched.append(base)
  190. if unmatched:
  191. raise ValueError(f"Missing {predict} for {', '.join(unmatched)}")
  192. tau_cols_final = [pair[0] for pair in matched]
  193. thickness_cols_final = [pair[1] for pair in matched]
  194. rois = [c.replace(f"_{independent}", "") for c in tau_cols_final]
  195. return df[tau_cols_final], df[thickness_cols_final], rois
  196. def compute_bisquare_residuals(self, tau_df, thickness_df, log_transform=True, cov_df=None, covariates=None):
  197. n_subjects, n_rois = tau_df.shape
  198. residuals = np.zeros((n_subjects, n_rois))
  199. roi_cov_usage = {} # track ROI-specific covariate usage: {cov: set(roi_idx)}
  200. global_cov_used = set() # track used global covariates
  201. missing_globals = set() # track skipped global covariates
  202. all_roi_specific_cov = set() # track all specified ROI-specific covariates (e.g., "amyloid")
  203. for i in range(n_rois):
  204. tau = tau_df.iloc[:, i]
  205. if log_transform:
  206. tau = np.log10(tau)
  207. thick = thickness_df.iloc[:, i]
  208. mask = tau.notna() & thick.notna()
  209. # Base design matrix with tau
  210. X = pd.DataFrame({'tau': tau})
  211. if covariates and cov_df is not None:
  212. for cov in covariates:
  213. if cov.startswith("*"):
  214. base = cov[1:]
  215. all_roi_specific_cov.add(base)
  216. roi_col = tau_df.columns[i].replace("_tau", f"_{base}")
  217. if roi_col in cov_df.columns:
  218. X[cov] = cov_df[roi_col]
  219. roi_cov_usage.setdefault(base, set()).add(i)
  220. else:
  221. if cov in cov_df.columns:
  222. X[cov] = cov_df[cov]
  223. global_cov_used.add(cov)
  224. else:
  225. missing_globals.add(cov)
  226. # Ensure numeric
  227. non_numeric_cols = X.select_dtypes(exclude=["number"]).columns
  228. if len(non_numeric_cols) > 0:
  229. raise TypeError(f"❌ Non-numeric covariates detected in model for ROI {tau_df.columns[i]}: {', '.join(non_numeric_cols)}")
  230. try:
  231. X_masked = sm.add_constant(X[mask])
  232. y = thick[mask]
  233. rlm_model = sm.RLM(y, X_masked, M=sm.robust.norms.TukeyBiweight()).fit()
  234. pred = rlm_model.predict(sm.add_constant(X))
  235. residuals[:, i] = thick - pred
  236. except Exception as e:
  237. print(f"⚠️ RLM failed for ROI {tau_df.columns[i]}: {e}")
  238. residuals[:, i] = np.nan
  239. # === Summary messages ===
  240. if global_cov_used:
  241. print(f" - Global covariates used: {', '.join(sorted(global_cov_used))}")
  242. if missing_globals:
  243. print(f" - ⚠️ Global covariates NOT found and skipped: {', '.join(sorted(missing_globals))}")
  244. for base in sorted(all_roi_specific_cov):
  245. if base not in roi_cov_usage:
  246. print(f" - ⚠️ ROI-specific covariate NOT found in any ROI: {base}")
  247. elif len(roi_cov_usage[base]) == n_rois:
  248. print(f" - All ROIs used ROI-specific covariate: {base}")
  249. else:
  250. print(f" - ROI-specific covariate used in {len(roi_cov_usage[base])} out of {n_rois} ROIs: {base}")
  251. return pd.DataFrame(residuals, columns=thickness_df.columns)
  252. #def binarize_residuals(self, res_df, sd_thresh):
  253. # if sd_thresh == 0:
  254. # return res_df
  255. # bin_df = pd.DataFrame(index=res_df.index, columns=res_df.columns)
  256. # for col in res_df.columns:
  257. # sd = np.nanstd(res_df[col])
  258. # bin_df[col] = res_df[col].apply(lambda x: -1 if x < -sd_thresh * sd else (1 if x > sd_thresh * sd else 0))
  259. # return bin_df.fillna(0)
  260. def binarize_residuals(self, res_df, sd_thresh, feature_weights=None):
  261. if sd_thresh == 0:
  262. return res_df
  263. bin_df = pd.DataFrame(index=res_df.index, columns=res_df.columns)
  264. for col in res_df.columns:
  265. sd = np.nanstd(res_df[col])
  266. bin_df[col] = res_df[col].apply(
  267. lambda x: -1 if x < -sd_thresh * sd else (1 if x > sd_thresh * sd else 0)
  268. )
  269. bin_df = bin_df.fillna(0)
  270. if feature_weights:
  271. for col in bin_df.columns:
  272. base = col.split("_")[0] # Extract '1002' from '1002_thickness'
  273. weight = feature_weights.get(base)
  274. if weight is not None:
  275. bin_df[col] = bin_df[col].astype(float) * weight
  276. unmatched = set(feature_weights.keys()) - set([col.split("_")[0] for col in bin_df.columns])
  277. if unmatched:
  278. print(f"⚠️ Warning: These features in --feature_weights were not matched to any residual columns: {', '.join(unmatched)}")
  279. return bin_df
  280. def run_hierarchical_clustering(self, bin_df, n_clusters=None):
  281. distance_matrix = pdist(bin_df.values, metric="euclidean")
  282. linkage_matrix = linkage(distance_matrix, method="ward")
  283. if n_clusters is None:
  284. distances = linkage_matrix[:, 2]
  285. diff = np.diff(distances[::-1])
  286. elbow_index = np.argmax(diff[:9]) + 1
  287. n_clusters = elbow_index + 1
  288. print("\n🔎 Auto-selecting number of clusters based on elbow method...")
  289. print(f"📐 Optimal number of clusters estimated: {n_clusters}")
  290. cluster_labels = fcluster(linkage_matrix, n_clusters, criterion="maxclust")
  291. return cluster_labels, linkage_matrix
  292. def determine_cluster_type_all_clusters(self, cluster_means, cluster_sizes):
  293. canonical_id = min(
  294. cluster_means.keys(),
  295. key=lambda cid: (np.nanmean(np.abs(cluster_means[cid])), -cluster_sizes[cid])
  296. )
  297. canonical_profile = cluster_means[canonical_id]
  298. cluster_types = {}
  299. for cid, mean_profile in cluster_means.items():
  300. if cid == canonical_id:
  301. cluster_types[cid] = "Likely Canonical"
  302. else:
  303. mean_residual = np.nanmean(mean_profile)
  304. canonical_mean_residual = np.nanmean(canonical_profile)
  305. if mean_residual < canonical_mean_residual:
  306. cluster_types[cid] = "Likely Vulnerable"
  307. else:
  308. cluster_types[cid] = "Likely Resilient"
  309. return cluster_types
  310. def map_residuals_to_nifti(self, cluster_id, mean_resid, rois, out_path):
  311. seg_img = nib.load(self.seg_path)
  312. seg_data = seg_img.get_fdata()
  313. out_data = np.zeros_like(seg_data, dtype=np.float32)
  314. roi_map = {int(r): mean_resid[i] for i, r in enumerate(rois)}
  315. for label, value in roi_map.items():
  316. out_data[seg_data == label] = value
  317. out_img = nib.Nifti1Image(out_data, affine=seg_img.affine, header=seg_img.header)
  318. nib.save(out_img, out_path)
  319. def plot_heatmap(self, df, out_path, rois=None, title=None):
  320. plt.figure(figsize=(max(20, df.shape[1] * 0.3), max(5, df.shape[0] * 0.3)))
  321. sns.heatmap(df, cmap="vlag", center=0, cbar_kws={"label": "Residual"},
  322. xticklabels=rois if rois else True, yticklabels=False)
  323. plt.xticks(rotation=45, ha='right', fontsize=6)
  324. plt.title(title or "Residual Heatmap")
  325. plt.tight_layout()
  326. plt.savefig(out_path)
  327. plt.close()
  328. def print_centered(self, text):
  329. try:
  330. width = os.get_terminal_size().columns
  331. except OSError:
  332. width = 100
  333. print(text.center(width))
  334. @staticmethod
  335. def report_pairwise_correlations(tau_df, thickness_df, rois):
  336. sig_count = 0
  337. trend_count = 0
  338. total = len(rois)
  339. for i in range(total):
  340. tau = tau_df.iloc[:, i]
  341. thick = thickness_df.iloc[:, i]
  342. mask = tau.notna() & thick.notna()
  343. if mask.sum() < 3:
  344. continue
  345. _, pval = pearsonr(tau[mask], thick[mask])
  346. if pd.notna(pval):
  347. if pval < 0.05:
  348. sig_count += 1
  349. elif pval < 0.1:
  350. trend_count += 1
  351. print(f"\n Pearson correlation summary between independent and dependent variables:")
  352. print(f" - {sig_count} out of {total} ROIs show significant correlation (p < 0.05)")
  353. print(f" - {trend_count} out of {total} ROIs show a trend (0.05 < p < 0.1)")
  354. if (sig_count + trend_count) < (0.3 * total):
  355. print(" ⚠️ Caution: Looks like the correlation between your variables is on the low side.")
  356. print(" Keep in mind - Mismatch modeling works best when there's at least some relationship!")
  357. def plot_dendrogram(self, linkage_matrix, labels, out_path):
  358. plt.figure(figsize=(12, 6))
  359. dendrogram(
  360. linkage_matrix,
  361. labels=labels,
  362. leaf_rotation=90,
  363. leaf_font_size=8,
  364. color_threshold=linkage_matrix[-(len(set(labels))-1), 2] if len(set(labels)) > 1 else 0
  365. )
  366. plt.title("Hierarchical Clustering Dendrogram")
  367. plt.xlabel("Subject Index")
  368. plt.tight_layout()
  369. plt.savefig(out_path)
  370. plt.close()
  371. print(f"🧬 Dendrogram saved to: {out_path}")
  372. def run_pipeline(self, input_csv, independent, predict, sd_thresh=1.5, n_clusters=None,
  373. output_prefix="output", log_transform=True, generate_maps=True, covariates=None, feature_weights=None):
  374. self.print_centered("🌟 Welcome to the Mismatch Universe! 🌟")
  375. self.print_centered("🧠 PATCH Lab warmly welcomes you to explore brain heterogenity together!! 😊")
  376. df = pd.read_csv(input_csv)
  377. ids = df["ID"]
  378. tau_df, thickness_df, rois = self.extract_matched_tau_thickness(df, independent, predict)
  379. print(f"🗺️ Using predefined atlas: {self.atlas}")
  380. print(f"👤 Number of subjects: {df.shape[0]}")
  381. print(f"🧠 Number of ROIs: {len(rois)}")
  382. self.report_pairwise_correlations(tau_df, thickness_df, rois)
  383. print("📈 Running robust bisquare regression...")
  384. residuals = self.compute_bisquare_residuals(tau_df, thickness_df, log_transform=log_transform,
  385. cov_df=df, covariates=covariates)
  386. residuals.insert(0, "ID", ids)
  387. print(f"📊 Using residuals for clustering (SD={sd_thresh})...")
  388. binary_df = self.binarize_residuals(residuals.drop(columns="ID"), sd_thresh, feature_weights=feature_weights)
  389. print("🔗 Running Ward.D2 clustering...")
  390. labels, linkage_matrix = self.run_hierarchical_clustering(binary_df, n_clusters)
  391. residuals["T_N_cluster"] = labels
  392. global_mismatch_scores = residuals.drop(columns=["ID", "T_N_cluster"]).sum(axis=1) * -1
  393. result = pd.DataFrame({"ID": ids, "T_N_cluster": labels,
  394. "Global_Mismatch_Score": global_mismatch_scores})
  395. print("\n📊 Cluster Membership Counts:")
  396. for cid, count in result["T_N_cluster"].value_counts().sort_index().items():
  397. print(f" Cluster {cid}: {count} subjects")
  398. heatmap_dir = f"{output_prefix}_cluster_heatmaps"
  399. os.makedirs(heatmap_dir, exist_ok=True)
  400. cluster_means = {}
  401. cluster_sizes = {}
  402. cluster_heatmap_paths = []
  403. cluster_nii_paths = []
  404. roi_names = [self.label_map.get(int(r), r) for r in rois] if self.label_map else rois
  405. #roi_names = [self.label_map.get(r, r) for r in rois] if self.label_map else rois
  406. for cid in sorted(set(labels)):
  407. cluster_df = residuals[residuals["T_N_cluster"] == cid]
  408. cluster_means[cid] = cluster_df.drop(columns=["ID", "T_N_cluster"]).mean()
  409. cluster_sizes[cid] = len(cluster_df)
  410. heatmap_path = os.path.join(heatmap_dir, f"cluster_{cid}_residuals.png")
  411. cluster_heatmap_paths.append((cid, heatmap_path))
  412. self.plot_heatmap(
  413. cluster_df.drop(columns=["ID", "T_N_cluster"]),
  414. heatmap_path,
  415. roi_names,
  416. f"Residual Heatmap - Cluster {cid}"
  417. )
  418. print(f"🖼️ Residual heatmap saved for Cluster {cid} at: {heatmap_path}")
  419. if generate_maps and self.seg_path:
  420. mean_resid = cluster_means[cid].values
  421. nii_path = os.path.join(heatmap_dir, f"cluster_{cid}_residual_map.nii.gz")
  422. self.map_residuals_to_nifti(cid, mean_resid, rois, nii_path)
  423. print(f"🧠 Residual map for cluster {cid} saved to: {nii_path}")
  424. cluster_nii_paths.append((cid, nii_path))
  425. residuals_path = f"{output_prefix}_residuals.csv"
  426. residuals.to_csv(residuals_path, index=False)
  427. print(f"📁 Residuals saved to: {residuals_path}")
  428. cluster_path = f"{output_prefix}_clusters.csv"
  429. result.to_csv(cluster_path, index=False)
  430. print(f"📁 Cluster labels saved to: {cluster_path}")
  431. print(f"🗺️ Using predefined atlas: {self.atlas}")
  432. cluster_types = self.determine_cluster_type_all_clusters(cluster_means, cluster_sizes)
  433. for cid, ctype in cluster_types.items():
  434. print(f"✅ Cluster {cid} is {ctype}")
  435. name_df = pd.DataFrame([(cid, cluster_types[cid]) for cid in cluster_types],
  436. columns=["T_N_cluster", "Cluster_Label"])
  437. final_result = result.merge(name_df, on="T_N_cluster")
  438. result_path = f"{output_prefix}_clusters_with_names.csv"
  439. final_result.to_csv(result_path, index=False)
  440. print(f"📁 Cluster labels with names saved to: {result_path}")
  441. full_heatmap = f"{output_prefix}_residual_heatmap_all.png"
  442. self.plot_heatmap(
  443. residuals.drop(columns=["ID", "T_N_cluster"]),
  444. full_heatmap,
  445. roi_names,
  446. "Residual Heatmap - All Subjects"
  447. )
  448. print(f"🖼️ Full residual heatmap saved to: {full_heatmap}")
  449. dendro_path = f"{output_prefix}_dendrogram.png"
  450. self.plot_dendrogram(linkage_matrix, labels=[str(i) for i in ids], out_path=dendro_path)
  451. return {
  452. "residual_df": residuals,
  453. "cluster_df": result,
  454. "final_df": final_result,
  455. "residual_csv": residuals_path,
  456. "cluster_csv": cluster_path,
  457. "final_csv": result_path,
  458. "cluster_heatmaps": cluster_heatmap_paths,
  459. "cluster_nifti_maps": cluster_nii_paths,
  460. "all_subjects_heatmap": full_heatmap,
  461. "dendrogram_path": dendro_path
  462. }

TNMismatchClustering copy.py at commit 4d3ec8b, under MIT · at the source

Overview

Authors: Xueying Lyu1, Nidhi S Mundada1, Christopher A Brown2, Niyousha Sadeghpour3, Emily McGrew3, Long Xie3, Yue Li3, Anika Wuestefeld4, Michael Tran Duong3, Philip Cook3, James Gee3, Ilya M Nasrallah3, Alice S Chen-Plotkin2, Leslie M Shaw1, Dawn Mechanic-Hamilton2, Laura E M Wisse4, Paul A Yushkevich3, Sandhitsu R Das2, David A Wolk2
  1. Department of Bioengineering, University of Pennsylvania, Philadelphia, PA 19104, USA
  2. Department of Neurology, University of Pennsylvania, Philadelphia, PA 19104, USA
  3. Department of Radiology, University of Pennsylvania, Philadelphia, PA 19104, USA
  4. Department of Clinical Sciences Lund, Lund University, Lund SE-221 00, Sweden
Institutions: University of Pennsylvania (United States); Lund University (Sweden)
Journal: Brain : a journal of neurology, volume 149, issue 11, pages 3890-3904
Dates: received 21 August 2025; accepted 14 February 2026; published online 28 April 2026; in print November 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1093/brain/awag105 · PMID 42046390 · PMCID PMC13480308 · OpenAlex W7156620307
Open access: hybrid, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: structural MRI / diffusion (modality), other (modality), human (organism), other condition (population), Alzheimer's / dementia (population), clinical / translational (subfield)
Methods: Statistics, Machine learning, Preprocessing, fMRI & imaging
Keywords: Alzheimer’s disease, medial temporal lobe, LATE-NC, resilience, plasma ptau217, mismatch
MeSH: Alzheimer Disease*, tau Proteins*, Temporal Lobe*, Aged, Aged, 80 and over, Atrophy, Biomarkers, Female, Humans, Magnetic Resonance Imaging, Male (* major topic)
Topic: Dementia and Cognitive Impairment Research (Psychiatry and Mental health, Medicine), according to OpenAlex
Citations: cited by 1 paper (Europe PMC); 64 references in the paper

Abstract

Although tau pathology is closely associated with neurodegeneration in Alzheimer’s disease, our prior work using multi-modality imaging revealed that mismatch between tau (T) and neurodegeneration (N) might reflect contributions from non-Alzheimer’s disease processes. The medial temporal lobe (MTL), an early site of Alzheimer’s disease pathology, is also a common target of co-pathologies, such as limbic-predominant age-related TDP-43 encephalopathy neuropathological change (LATE-NC), often following an anterior-to-posterior atrophy gradient. Given the susceptibility of MTL to co-pathologies, here we explored T-N mismatch specifically within MTL using plasma ptau217 and MTL morphometry for identifying vulnerabilities and resilience in cognitively impaired or unimpaired patients with Alzheimer’s disease.

We parcellated the MTL into 100 spatially contiguous segments and calculated their T-N mismatch using plasma ptau217 as a measure for T and thickness as a marker of N. Based on these mismatch profiles, we clustered 447 amyloid-positive individuals from the Alzheimer’s Disease Neuroimaging Initiative cohort into data-driven T-N phenotypes. We characterized the T-N phenotypes by examining their cross-sectional and longitudinal atrophy both within the MTL and across the whole brain, in addition to cognitive trajectories. This framework was replicated in an independent cohort and, finally, translated to a real-world clinical sample of 50 patients undergoing anti-amyloid therapy.

Clustering identified three T-N phenotypes with different MTL T-N mismatch profiles, atrophy patterns and cognitive outcomes, despite comparable Alzheimer’s disease severity. The ‘canonical’ group, characterized by low T-N residuals (N ∼ T), showed Alzheimer’s disease-like neurodegeneration patterns. The ‘vulnerable’ group, characterized by disproportionately greater neurodegeneration than tau (N > T), showed atrophy primarily in the anterior MTL that extended into temporal-limbic regions, in both cross-sectional and longitudinal analyses. This group also exhibited neurodegeneration that preceded estimated tau onset and experienced faster cognitive decline across multiple domains, aligning with the typical characteristics of mixed LATE-NC with Alzheimer’s disease. In contrast, the ‘resilient’ group (N < T) showed minimal atrophy and preserved cognitive function. These phenotypes were reproducible in an independent research cohort. Importantly, in a feasibility study applying the model developed from the Alzheimer’s Disease Neuroimaging Initiative to a clinical cohort of patients receiving lecanemab, we identified vulnerable individuals with LATE-like atrophy patterns. This highlights its potential utility for identifying individuals with co-pathology in clinical settings.

Our findings demonstrate that T-N mismatch within MTL using MRI and plasma biomarkers can reveal Alzheimer’s disease groups with varying vulnerability/resilience, with the vulnerable group displaying structural and cognitive outcomes suggestive of LATE-NC. This approach offers a cost-effective strategy for clinical trial stratification and precision medicine for Alzheimer’s disease therapeutics.

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

Repositories

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

xueying-lyu/mismatch

License: MIT
State: the link answers, verified on 4 November 2026
Evidence: files inventoried
Commit: 4d3ec8b02134232871c0e7b73c67aed41f7bb992, 19 August 2025
Languages: Python (9), Jupyter (1)
Size: 62 files, 10 scripts
Software Heritage: not archived
Found in: the text, “Conclusion”
Holds: README, license file, environment (pyproject.toml, setup.cfg), tests, continuous integration, 1 notebook
Not found: CITATION.cff, documentation
Tools: NumPy (7 files), pandas (7 files), NiBabel (3 files), Matplotlib (2 files), scikit-learn (2 files), SciPy (2 files), seaborn (2 files), statsmodels (2 files), Nilearn (1 file)
Availability: 1 check, the latest on 4 November 2026: the link answers
  • 4 November 2026: the link answers
12 files

pyushkevich/cmrep

License: GPL-3.0
State: the link answers, verified on 28 October 2026
Evidence: files inventoried
Commit: c0760b45a8fcb1358619e46fc5c09b1f6e34e7b8, 3 February 2026
Languages: C/C++ (75), MATLAB (17), C (5), C++ (2), Python (1), Jupyter (1), Shell (1)
Size: 314 files, 102 scripts
Software Heritage: archived
Found in: the text, “MTL surface vertex-wise thickness comparisons”
Holds: README, license file, environment (Dockerfile), documentation, 1 notebook
Not found: CITATION.cff, tests, continuous integration
Tools: Image Processing Toolbox (4 files), NumPy (1 file), PyTorch (1 file)
Availability: 2 checks, the latest on 28 October 2026: the link answers
  • 28 October 2026: the link answers
  • 27 September 2026: the link answers
104 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;
  • 112 scripts, each with its path and the digest of its content;
  • 6 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

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

Data

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

Data availability

Data from ADNI used in this study are publicly available at adni.loni.usc.edu. Data from the Penn ADRC and ATM cohorts are available from the corresponding author upon reasonable request.

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

Versions

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

Version 1, 4 November 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 19 authors, 6 keywords, 11 MeSH terms, 44 funders, 0 references.

Cite

This paper

Lyu, X., Mundada, N. S., Brown, C. A., Sadeghpour, N., McGrew, E., Xie, L., Li, Y., Wuestefeld, A., Duong, M. T., Cook, P., Gee, J., Nasrallah, I. M., Chen-Plotkin, A. S., Shaw, L. M., Mechanic-Hamilton, D., Wisse, L. E. M., Yushkevich, P. A., Das, S. R., & Wolk, D. A. (2026). Medial temporal lobe tau-neurodegeneration mismatch from structural imaging and plasma biomarkers. Brain : a journal of neurology, 149(11), 3890-3904. https://doi.org/10.1093/brain/awag105

BibTeX

@article{lyu2026medial,
author = {Lyu, Xueying and Mundada, Nidhi S and Brown, Christopher A and Sadeghpour, Niyousha and McGrew, Emily and Xie, Long and Li, Yue and Wuestefeld, Anika and Duong, Michael Tran and Cook, Philip and Gee, James and Nasrallah, Ilya M and Chen-Plotkin, Alice S and Shaw, Leslie M and Mechanic-Hamilton, Dawn and Wisse, Laura E M and Yushkevich, Paul A and Das, Sandhitsu R and Wolk, David A},
title = {{Medial temporal lobe tau-neurodegeneration mismatch from structural imaging and plasma biomarkers}},
journal = {Brain : a journal of neurology},
year = {2026},
month = nov,
volume = {149},
number = {11},
pages = {3890--3904},
publisher = {Oxford University Press},
issn = {0006-8950},
doi = {10.1093/brain/awag105},
url = {https://doi.org/10.1093/brain/awag105},
pmid = {42046390},
pmcid = {PMC13480308}
}

RIS

TY - JOUR
AU - Lyu, Xueying
AU - Mundada, Nidhi S
AU - Brown, Christopher A
AU - Sadeghpour, Niyousha
AU - McGrew, Emily
AU - Xie, Long
AU - Li, Yue
AU - Wuestefeld, Anika
AU - Duong, Michael Tran
AU - Cook, Philip
AU - Gee, James
AU - Nasrallah, Ilya M
AU - Chen-Plotkin, Alice S
AU - Shaw, Leslie M
AU - Mechanic-Hamilton, Dawn
AU - Wisse, Laura E M
AU - Yushkevich, Paul A
AU - Das, Sandhitsu R
AU - Wolk, David A
TI - Medial temporal lobe tau-neurodegeneration mismatch from structural imaging and plasma biomarkers
T2 - Brain : a journal of neurology
J2 - Brain
PY - 2026
DA - 2026/11/01
VL - 149
IS - 11
SP - 3890
EP - 3904
SN - 0006-8950
PB - Oxford University Press
DO - 10.1093/brain/awag105
UR - https://doi.org/10.1093/brain/awag105
LA - en
ER -

CSL-JSON

{
"id": "10.1093/brain/awag105",
"type": "article-journal",
"title": "Medial temporal lobe tau-neurodegeneration mismatch from structural imaging and plasma biomarkers",
"container-title": "Brain : a journal of neurology",
"author": [
{
"family": "Lyu",
"given": "Xueying"
},
{
"family": "Mundada",
"given": "Nidhi S"
},
{
"family": "Brown",
"given": "Christopher A"
},
{
"family": "Sadeghpour",
"given": "Niyousha"
},
{
"family": "McGrew",
"given": "Emily"
},
{
"family": "Xie",
"given": "Long"
},
{
"family": "Li",
"given": "Yue"
},
{
"family": "Wuestefeld",
"given": "Anika"
},
{
"family": "Duong",
"given": "Michael Tran"
},
{
"family": "Cook",
"given": "Philip"
},
{
"family": "Gee",
"given": "James"
},
{
"family": "Nasrallah",
"given": "Ilya M"
},
{
"family": "Chen-Plotkin",
"given": "Alice S"
},
{
"family": "Shaw",
"given": "Leslie M"
},
{
"family": "Mechanic-Hamilton",
"given": "Dawn"
},
{
"family": "Wisse",
"given": "Laura E M"
},
{
"family": "Yushkevich",
"given": "Paul A"
},
{
"family": "Das",
"given": "Sandhitsu R"
},
{
"family": "Wolk",
"given": "David A"
}
],
"container-title-short": "Brain",
"volume": "149",
"issue": "11",
"page": "3890-3904",
"DOI": "10.1093/brain/awag105",
"PMID": "42046390",
"PMCID": "PMC13480308",
"ISSN": "0006-8950",
"publisher": "Oxford University Press",
"URL": "https://doi.org/10.1093/brain/awag105",
"language": "en",
"issued": {
"date-parts": [
[
2026,
11,
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.1002/hipo.70027 [code]
Developing an Anatomically Valid Segmentation Protocol for Anterior Regions of the Medial Temporal Lobe for Neurodegenerative Diseases.
Journal: Hippocampus
In common: NiBabel, pandas, Matplotlib, 1 other tool, Alzheimer's / dementia, structural MRI / diffusion, other condition, 2 authors
[2] doi:10.1002/alz.71649 [code]
Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: statsmodels, NiBabel, PyTorch, 6 other tools, Alzheimer's / dementia, structural MRI / diffusion, other condition, author Michael Tran Duong
[3] doi:10.1038/s41467-024-49205-0 [code]
Postmortem imaging reveals patterns of medial temporal lobe vulnerability to tau pathology in Alzheimer's disease.
Journal: Nature communications
In common: NiBabel, PyTorch, pandas, 3 other tools, Alzheimer's / dementia, structural MRI / diffusion, clinical / translational, author Paul A Yushkevich
[4] doi:10.1002/alz.14492 [code]
A multi-cohort study of longitudinal and cross-sectional Alzheimer's disease biomarkers in cognitively unimpaired older adults.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: NiBabel, Image Processing Toolbox, PyTorch, 2 other tools, Alzheimer's / dementia, structural MRI / diffusion, clinical / translational, author Christopher A Brown
[5] doi:10.1093/braincomms/fcag169 [code]
Clinicoanatomic localization of iron-rich gliosis in aphasic presentations of globular glial tauopathy.
Journal: Brain communications
In common: NiBabel, pandas, SciPy, 2 other tools, Alzheimer's / dementia, structural MRI / diffusion, author Paul A Yushkevich
[6] doi:10.1007/s00401-024-02789-9 [code]
Association of quantitative histopathology measurements with antemortem medial temporal lobe cortical thickness in the Alzheimer's disease continuum.
Journal: Acta neuropathologica
In common: Image Processing Toolbox, PyTorch, NumPy, Alzheimer's / dementia, structural MRI / diffusion, author Christopher A Brown
[7] doi:10.1162/imag.a.1053
Fully individualized models for cross-sectional and longitudinal network-based tau spread.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Alzheimer's / dementia, structural MRI / diffusion, author Christopher A Brown
[8] doi:10.1093/braincomms/fcad245
Aging and Alzheimer's disease have dissociable effects on local and regional medial temporal lobe connectivity.
Journal: Brain communications
In common: Alzheimer's / dementia, author Paul A Yushkevich
[9] doi:10.1002/hbm.26326
Young versus older subject diffusion magnetic resonance imaging data for virtual white matter lesion tractography.
Journal: Human brain mapping
In common: structural MRI / diffusion, author Paul A Yushkevich

Contribute

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

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

Add the badge to the code's README

OSCR: the badge of this paper

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

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.1093/brain/awag105.svg)](https://openscicode.org/badge/doi/10.1093/brain/awag105)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.1093/brain/awag105.svg
   :target: https://openscicode.org/badge/doi/10.1093/brain/awag105
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1093/brain/awag105"><img src="https://openscicode.org/badge/doi/10.1093/brain/awag105.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

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

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.

Request its removal

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

Discussion, reproductions, activity

Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

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

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

Report a reproduction failure (signed in)

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

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