OSCR

Fine-scale individualized gyral folding-based cortical similarity networks reveal distinct organizational patterns in Alzheimer's disease and Lewy body dementia.

Code ↔ Paper

9 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 9 matches
  1. [1] § Methods › Cortical thickness-constrained arealization ↔ src/landmark_mind_from_vtk_mdkde_adaptive.py, lines 1–42 · score 0.91 · Newly added vertices, morphological closing, post processing, geodesic distance, propagation, components
  2. [2] § Methods › Cortical thickness-constrained arealization ↔ src/landmark_mind_from_vtk_mdkde_adaptive.py, lines 1–42 · score 0.80 · multiple gating, geodesic distance, multiple seeds, reachable, Smaller, propagation
  3. [3] § Methods › Network metrics › Controlling for network size ↔ src/compare_node_number_effect_full.py, lines 4–56 · score 0.65 · clustering coefficient, topological metrics, global efficiency, sex, age, covariate
  4. [4] § Results › Group differences in graph theoretical metrics ↔ src/compare_node_number_effect_full.py, lines 4–56 · score 0.63 · small worldness, degree assortativity, global efficiency, GLM, transitivity, clustering
  5. [5] § Results › Classification performance of topology and node-count features ↔ src/classification_nestedcv_residN.py, lines 1–32 · score 0.61 · ROC curves, linear SVM, topology residualized, classification, nested
  6. [6] § Methods › Network metrics › Distribution-level metrics ↔ src/compare_node_number_effect_full.py, lines 58–97 · score 0.60 · nodal efficiency inequality, degree hubness ratio, Gini, coefficient, node
  7. [7] § Results › Group differences in graph theoretical metrics ↔ src/compare_node_number_effect_full.py, lines 58–97 · score 0.57 · nodal efficiency inequality, degree hubness ratio, degree inequality, node, graph
  8. [8] § Methods › Landmark-based cortical similarity network construction ↔ src/compare_node_number_effect_full.py, lines 145–221 · score 0.55 · minimum spanning tree, MST, weighted, thresholding, edges, matrix
  9. [9] § Results › Classification performance of topology and node-count features ↔ src/classification_nestedcv_residN.py, lines 477–552 · score 0.54 · residualized topological, bootstrap, inner, CI, outer, permutation

Paper

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

The paper is loaded when this pane is shown.

The authors' code

Python · 842 lines · 26 KB · MIT · 5 matches

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Compare GLM results with and without node-number adjustment
  5. for both:
  6. 1) global topology metrics
  7. 2) local-distribution summary metrics
  8. Main outputs:
  9. - global_metrics_per_subject.tsv
  10. - local_metrics_per_subject.tsv
  11. - global_glm_compare_node.tsv
  12. - local_glm_compare_node.tsv
  13. - combined_glm_compare_node.tsv
  14. - combined_glm_compare_node_pretty.tsv
  15. Example:
  16. python compare_node_number_effect_full.py \
  17. --in-npy all_subjects.npy \
  18. --outdir compare_node_effect_out \
  19. --threshold-mode mst_top \
  20. --top-percent 10 \
  21. --covar-file covars.tsv \
  22. --covar-cols tiv
  23. Notes:
  24. - labels: 2=AD, 3=LBD
  25. - GLM coding: dx_AD = 1 for AD, 0 for LBD
  26. - Default base covariates: age + sex
  27. - Optional extra covariates from covar file, e.g. TIV
  28. - density is included by default in GLM comparison to match graph-level adjustment
  29. - Node-number effect is specifically compared by adding/removing N
  30. """
  31. import argparse
  32. from pathlib import Path
  33. import numpy as np
  34. import pandas as pd
  35. import networkx as nx
  36. LABEL_NAME = {0: "NC", 1: "MCI", 2: "AD", 3: "LBD"}
  37. PRETTY_LABELS = {
  38. # ---- global ----
  39. "global_efficiency": "Global efficiency",
  40. "char_path_length": "Characteristic path length",
  41. "clustering": "Average clustering coefficient",
  42. "transitivity": "Transitivity",
  43. "modularity": "Modularity",
  44. "assortativity": "Degree assortativity",
  45. "smallworld_sigma": "Small-worldness (σ)",
  46. "richclub_mean": "Rich-club coefficient (mean)",
  47. "richclub_max": "Rich-club coefficient (max)",
  48. "local_eff_mean": "Local efficiency (mean across nodes)",
  49. "local_eff_std": "Local efficiency (SD across nodes)",
  50. # ---- local summary ----
  51. "eff_mean": "Nodal efficiency (mean)",
  52. "eff_std": "Nodal efficiency (SD)",
  53. "eff_q10": "Nodal efficiency (10th percentile)",
  54. "eff_q50": "Nodal efficiency (median)",
  55. "eff_q90": "Nodal efficiency (90th percentile)",
  56. "eff_gini": "Nodal efficiency inequality (Gini)",
  57. "eff_top10_mean": "Nodal efficiency (top 10% mean)",
  58. "clust_mean": "Clustering coefficient (mean)",
  59. "clust_std": "Clustering coefficient (SD)",
  60. "clust_q10": "Clustering coefficient (10th percentile)",
  61. "clust_q50": "Clustering coefficient (median)",
  62. "clust_q90": "Clustering coefficient (90th percentile)",
  63. "clust_gini": "Clustering inequality (Gini)",
  64. "clust_top10_mean": "Clustering (top 10% mean)",
  65. "btw_mean": "Betweenness centrality (mean)",
  66. "btw_std": "Betweenness centrality (SD)",
  67. "btw_q10": "Betweenness centrality (10th percentile)",
  68. "btw_q50": "Betweenness centrality (median)",
  69. "btw_q90": "Betweenness centrality (90th percentile)",
  70. "btw_gini": "Betweenness inequality (Gini)",
  71. "btw_top10_mean": "Betweenness (top 10% mean)",
  72. "deg_mean": "Degree (mean)",
  73. "deg_std": "Degree (SD)",
  74. "deg_q10": "Degree (10th percentile)",
  75. "deg_q50": "Degree (median)",
  76. "deg_q90": "Degree (90th percentile)",
  77. "deg_gini": "Degree inequality (Gini)",
  78. "deg_top10_mean": "Degree (top 10% mean)",
  79. "deg_top10_mean_ratio": "Degree hubness ratio (top 10% / mean)",
  80. # ---- generic ----
  81. "density": "Graph density",
  82. "N": "Number of nodes",
  83. "E": "Number of edges",
  84. "avg_degree": "Average degree",
  85. }
  86. # =========================================================
  87. # Utils
  88. # =========================================================
  89. def _safe_float(x):
  90. try:
  91. if x is None:
  92. return np.nan
  93. v = float(x)
  94. return v if np.isfinite(v) else np.nan
  95. except Exception:
  96. return np.nan
  97. def fmt_num(x, nd=4):
  98. if x is None or not np.isfinite(x):
  99. return "NA"
  100. return f"{x:.{nd}f}"
  101. def fmt_p(x):
  102. if x is None or not np.isfinite(x):
  103. return "NA"
  104. if x < 1e-4:
  105. return "<1e-4"
  106. if x < 1e-3:
  107. return f"{x:.1e}"
  108. return f"{x:.4f}"
  109. def significance_flag(p, q, alpha=0.05):
  110. if not np.isfinite(q):
  111. return "NA"
  112. return "Yes" if q <= alpha else "No"
  113. # =========================================================
  114. # Thresholding
  115. # =========================================================
  116. def _upper_tri_edges(W: np.ndarray):
  117. iu = np.triu_indices(W.shape[0], k=1)
  118. return iu[0], iu[1], W[iu]
  119. def build_graph(W: np.ndarray, mode: str, top_percent: float, abs_thr: float | None) -> nx.Graph:
  120. W = np.asarray(W, dtype=np.float64)
  121. if W.ndim != 2 or W.shape[0] != W.shape[1]:
  122. raise ValueError(f"Matrix must be square, got {W.shape}")
  123. W = (W + W.T) / 2.0
  124. np.fill_diagonal(W, 0.0)
  125. W[~np.isfinite(W)] = 0.0
  126. n = W.shape[0]
  127. G = nx.Graph()
  128. G.add_nodes_from(range(n))
  129. ii, jj, ww = _upper_tri_edges(W)
  130. if mode == "none":
  131. keep = ww > 0
  132. for a, b, w in zip(ii[keep], jj[keep], ww[keep]):
  133. G.add_edge(int(a), int(b), weight=float(w))
  134. return G
  135. if mode == "abs":
  136. if abs_thr is None:
  137. raise ValueError("--abs-thr required for abs mode")
  138. keep = ww >= float(abs_thr)
  139. for a, b, w in zip(ii[keep], jj[keep], ww[keep]):
  140. if w > 0:
  141. G.add_edge(int(a), int(b), weight=float(w))
  142. return G
  143. if top_percent <= 0 or top_percent > 100:
  144. raise ValueError("--top-percent must be in (0,100].")
  145. p = top_percent / 100.0
  146. m_total = ww.size
  147. k_keep = max(int(np.ceil(p * m_total)), 1)
  148. order = np.argsort(-ww)
  149. top_idx = order[:k_keep]
  150. if mode == "top":
  151. for idx in top_idx:
  152. a, b, w = int(ii[idx]), int(jj[idx]), float(ww[idx])
  153. if w > 0:
  154. G.add_edge(a, b, weight=w)
  155. return G
  156. if mode == "mst_top":
  157. H = nx.Graph()
  158. H.add_nodes_from(range(n))
  159. for a, b, w in zip(ii, jj, ww):
  160. if w <= 0:
  161. continue
  162. dist = float(1.0 - w)
  163. H.add_edge(int(a), int(b), dist=dist, sim=float(w))
  164. if H.number_of_edges() == 0:
  165. return G
  166. T = nx.minimum_spanning_tree(H, weight="dist", algorithm="kruskal")
  167. for a, b, data in T.edges(data=True):
  168. G.add_edge(int(a), int(b), weight=float(data.get("sim", 0.0)))
  169. have = set((min(u, v), max(u, v)) for u, v in G.edges())
  170. for idx in top_idx:
  171. a, b = int(ii[idx]), int(jj[idx])
  172. key = (min(a, b), max(a, b))
  173. if key in have:
  174. continue
  175. w = float(ww[idx])
  176. if w <= 0:
  177. continue
  178. G.add_edge(a, b, weight=w)
  179. have.add(key)
  180. return G
  181. raise ValueError(f"Unknown threshold mode: {mode}")
  182. # =========================================================
  183. # Graph helpers
  184. # =========================================================
  185. def largest_connected_subgraph(G: nx.Graph) -> nx.Graph:
  186. if G.number_of_nodes() <= 1:
  187. return G.copy()
  188. if nx.is_connected(G):
  189. return G.copy()
  190. comps = list(nx.connected_components(G))
  191. if not comps:
  192. return G.copy()
  193. lcc = max(comps, key=len)
  194. return G.subgraph(lcc).copy()
  195. def nodal_efficiency(H: nx.Graph) -> np.ndarray:
  196. n = H.number_of_nodes()
  197. if n <= 1:
  198. return np.array([0.0], dtype=np.float64)
  199. nodes = list(H.nodes())
  200. idx = {v: i for i, v in enumerate(nodes)}
  201. eff = np.zeros(n, dtype=np.float64)
  202. for v in nodes:
  203. lengths = nx.single_source_shortest_path_length(H, v)
  204. inv = []
  205. for u, d in lengths.items():
  206. if u == v:
  207. continue
  208. if d > 0:
  209. inv.append(1.0 / float(d))
  210. eff[idx[v]] = float(np.mean(inv)) if inv else 0.0
  211. return eff
  212. def gini(x: np.ndarray) -> float:
  213. x = np.asarray(x, dtype=np.float64)
  214. x = x[np.isfinite(x)]
  215. if x.size == 0:
  216. return np.nan
  217. mn = x.min()
  218. if mn < 0:
  219. x = x - mn
  220. s = x.sum()
  221. if s <= 0:
  222. return 0.0
  223. x = np.sort(x)
  224. n = x.size
  225. i = np.arange(1, n + 1, dtype=np.float64)
  226. return float((2.0 * np.sum(i * x)) / (n * s) - (n + 1.0) / n)
  227. def summarize_distribution(x: np.ndarray, prefix: str, top_frac: float = 0.10) -> dict:
  228. x = np.asarray(x, dtype=np.float64)
  229. x = x[np.isfinite(x)]
  230. if x.size == 0:
  231. return {f"{prefix}_{k}": np.nan for k in ["mean", "std", "q10", "q50", "q90", "gini", "top10_mean"]}
  232. q10, q50, q90 = np.percentile(x, [10, 50, 90])
  233. topk = max(1, int(np.ceil(top_frac * x.size)))
  234. top_mean = float(np.mean(np.sort(x)[-topk:]))
  235. return {
  236. f"{prefix}_mean": float(np.mean(x)),
  237. f"{prefix}_std": float(np.std(x, ddof=0)),
  238. f"{prefix}_q10": float(q10),
  239. f"{prefix}_q50": float(q50),
  240. f"{prefix}_q90": float(q90),
  241. f"{prefix}_gini": float(gini(x)),
  242. f"{prefix}_top10_mean": float(top_mean),
  243. }
  244. # =========================================================
  245. # Global metrics
  246. # =========================================================
  247. def compute_global_metrics(G: nx.Graph, n_random: int = 20, seed: int = 0) -> dict:
  248. n = G.number_of_nodes()
  249. e = G.number_of_edges()
  250. density = (2.0 * e / (n * (n - 1))) if n >= 2 else 0.0
  251. avg_degree = (2.0 * e / n) if n > 0 else 0.0
  252. H = largest_connected_subgraph(G)
  253. n_lcc = H.number_of_nodes()
  254. geff = np.nan
  255. if n_lcc >= 2:
  256. geff = float(nx.global_efficiency(H))
  257. cpl = np.nan
  258. if n_lcc >= 2:
  259. try:
  260. cpl = float(nx.average_shortest_path_length(H))
  261. except Exception:
  262. cpl = np.nan
  263. try:
  264. clust = float(nx.average_clustering(G)) if n >= 2 else np.nan
  265. except Exception:
  266. clust = np.nan
  267. try:
  268. trans = float(nx.transitivity(G)) if n >= 3 else np.nan
  269. except Exception:
  270. trans = np.nan
  271. assort = np.nan
  272. if e > 0 and n >= 3:
  273. try:
  274. assort = float(nx.degree_assortativity_coefficient(G))
  275. except Exception:
  276. assort = np.nan
  277. modularity = np.nan
  278. if e > 0 and n >= 4:
  279. try:
  280. comms = nx.algorithms.community.greedy_modularity_communities(G)
  281. modularity = float(nx.algorithms.community.modularity(G, comms))
  282. except Exception:
  283. modularity = np.nan
  284. leff_mean = np.nan
  285. leff_std = np.nan
  286. if n >= 3:
  287. try:
  288. vals = []
  289. for v in G.nodes():
  290. nbrs = list(G.neighbors(v))
  291. if len(nbrs) < 2:
  292. vals.append(0.0)
  293. continue
  294. sub = G.subgraph(nbrs)
  295. vals.append(nx.global_efficiency(sub))
  296. vals = np.asarray(vals, dtype=np.float64)
  297. leff_mean = float(np.mean(vals))
  298. leff_std = float(np.std(vals, ddof=0))
  299. except Exception:
  300. leff_mean = np.nan
  301. leff_std = np.nan
  302. rc_mean = np.nan
  303. rc_max = np.nan
  304. if e > 0 and n >= 5:
  305. try:
  306. rc = nx.rich_club_coefficient(G, normalized=False, Q=100)
  307. vals = np.array([v for v in rc.values() if np.isfinite(v)], dtype=np.float64)
  308. if vals.size > 0:
  309. rc_mean = float(vals.mean())
  310. rc_max = float(vals.max())
  311. except Exception:
  312. rc_mean = np.nan
  313. rc_max = np.nan
  314. sw_sigma = np.nan
  315. if n_lcc >= 10 and H.number_of_edges() > 0:
  316. try:
  317. C = float(nx.average_clustering(H))
  318. L = float(nx.average_shortest_path_length(H))
  319. rng = np.random.default_rng(seed)
  320. deg_seq = [d for _, d in H.degree()]
  321. Cr, Lr = [], []
  322. for _ in range(n_random):
  323. CM = nx.configuration_model(deg_seq, seed=int(rng.integers(0, 1_000_000)))
  324. R = nx.Graph(CM)
  325. R.remove_edges_from(nx.selfloop_edges(R))
  326. R = largest_connected_subgraph(R)
  327. if R.number_of_nodes() < 10 or R.number_of_edges() == 0:
  328. continue
  329. Cr.append(nx.average_clustering(R))
  330. try:
  331. Lr.append(nx.average_shortest_path_length(R))
  332. except Exception:
  333. continue
  334. if len(Cr) >= max(5, n_random // 3) and len(Lr) >= max(5, n_random // 3):
  335. C_rand = float(np.mean(Cr))
  336. L_rand = float(np.mean(Lr))
  337. if C_rand > 0 and L_rand > 0:
  338. sw_sigma = float((C / C_rand) / (L / L_rand))
  339. except Exception:
  340. sw_sigma = np.nan
  341. return {
  342. "N": int(n),
  343. "E": int(e),
  344. "density": float(density),
  345. "avg_degree": float(avg_degree),
  346. "global_efficiency": geff,
  347. "char_path_length": cpl,
  348. "clustering": clust,
  349. "transitivity": trans,
  350. "modularity": modularity,
  351. "assortativity": assort,
  352. "local_eff_mean": leff_mean,
  353. "local_eff_std": leff_std,
  354. "richclub_mean": rc_mean,
  355. "richclub_max": rc_max,
  356. "smallworld_sigma": sw_sigma,
  357. }
  358. # =========================================================
  359. # Local summary metrics
  360. # =========================================================
  361. def compute_local_summary_metrics(G: nx.Graph) -> dict:
  362. n = G.number_of_nodes()
  363. ecount = G.number_of_edges()
  364. density = (2.0 * ecount / (n * (n - 1))) if n >= 2 else 0.0
  365. H = largest_connected_subgraph(G)
  366. deg = np.array([d for _, d in G.degree()], dtype=np.float64)
  367. clust = np.array(list(nx.clustering(G).values()), dtype=np.float64) if n >= 2 else np.zeros(n)
  368. btw = np.array(list(nx.betweenness_centrality(G, normalized=True).values()), dtype=np.float64) if n >= 3 else np.zeros(n)
  369. eff = nodal_efficiency(H)
  370. s_eff = summarize_distribution(eff, "eff")
  371. s_clu = summarize_distribution(clust, "clust")
  372. s_btw = summarize_distribution(btw, "btw")
  373. s_deg = summarize_distribution(deg, "deg")
  374. deg_mean = s_deg["deg_mean"]
  375. deg_top10_ratio = (s_deg["deg_top10_mean"] / deg_mean) if np.isfinite(deg_mean) and deg_mean != 0 else np.nan
  376. return {
  377. "N": int(n),
  378. "E": int(ecount),
  379. "density": float(density),
  380. **s_eff, **s_clu, **s_btw, **s_deg,
  381. "deg_top10_mean_ratio": float(deg_top10_ratio),
  382. }
  383. # =========================================================
  384. # Stats
  385. # =========================================================
  386. def bh_fdr(pvals: np.ndarray) -> np.ndarray:
  387. p = np.asarray(pvals, dtype=np.float64)
  388. q = np.full_like(p, np.nan)
  389. ok = np.isfinite(p)
  390. pv = p[ok]
  391. m = pv.size
  392. if m == 0:
  393. return q
  394. order = np.argsort(pv)
  395. ranked = pv[order]
  396. q_ranked = ranked * m / (np.arange(1, m + 1))
  397. q_ranked = np.minimum.accumulate(q_ranked[::-1])[::-1]
  398. q_ok = np.empty_like(pv)
  399. q_ok[order] = q_ranked
  400. q[ok] = np.clip(q_ok, 0, 1)
  401. return q
  402. def ols_fit(X: np.ndarray, y: np.ndarray):
  403. ok = np.isfinite(y) & np.isfinite(X).all(axis=1)
  404. X2 = X[ok]
  405. y2 = y[ok]
  406. n, k = X2.shape
  407. if n <= k + 1:
  408. raise ValueError(f"Too few samples after NaN drop: n={n}, k={k}")
  409. beta, *_ = np.linalg.lstsq(X2, y2, rcond=None)
  410. resid = y2 - X2 @ beta
  411. dof = n - k
  412. s2 = (resid @ resid) / dof
  413. XtX_inv = np.linalg.inv(X2.T @ X2)
  414. se = np.sqrt(np.diag(XtX_inv) * s2)
  415. t = beta / se
  416. try:
  417. from scipy import stats
  418. p = 2 * stats.t.sf(np.abs(t), dof)
  419. except Exception:
  420. from math import erf, sqrt
  421. def norm_sf(z):
  422. return 0.5 * (1 - erf(z / sqrt(2)))
  423. p = np.array([2 * norm_sf(abs(z)) for z in t], dtype=np.float64)
  424. return beta, se, t, p, dof, int(ok.sum())
  425. def run_glm_compare(df: pd.DataFrame, metric_cols: list[str], extra_covars: list[str], include_density: bool,
  426. fdr_alpha: float, analysis_name: str) -> pd.DataFrame:
  427. def _run_one(include_nodes: bool, model_name: str) -> pd.DataFrame:
  428. covars = ["dx_AD", "age", "sex"]
  429. for c in extra_covars:
  430. if c in df.columns and c not in covars:
  431. covars.append(c)
  432. if include_density and "density" in df.columns and "density" not in covars:
  433. covars.append("density")
  434. if include_nodes and "N" in df.columns and "N" not in covars:
  435. covars.append("N")
  436. X = df[covars].copy()
  437. X.insert(0, "intercept", 1.0)
  438. Xmat = X.to_numpy(dtype=np.float64)
  439. idx_dx = list(X.columns).index("dx_AD")
  440. rows = []
  441. for m in metric_cols:
  442. y = df[m].to_numpy(dtype=np.float64)
  443. try:
  444. beta, se, t, p, dof, n_used = ols_fit(Xmat, y)
  445. rows.append({
  446. "analysis": analysis_name,
  447. "metric": m,
  448. "pretty_metric": PRETTY_LABELS.get(m, m),
  449. "model": model_name,
  450. "include_N": int(include_nodes),
  451. "beta_dx_AD": float(beta[idx_dx]),
  452. "se_dx_AD": float(se[idx_dx]),
  453. "t_dx_AD": float(t[idx_dx]),
  454. "p_dx_AD": float(p[idx_dx]),
  455. "dof": int(dof),
  456. "n_used": int(n_used),
  457. "model_covars": ",".join(X.columns.tolist()),
  458. })
  459. except Exception as ex:
  460. rows.append({
  461. "analysis": analysis_name,
  462. "metric": m,
  463. "pretty_metric": PRETTY_LABELS.get(m, m),
  464. "model": model_name,
  465. "include_N": int(include_nodes),
  466. "beta_dx_AD": np.nan,
  467. "se_dx_AD": np.nan,
  468. "t_dx_AD": np.nan,
  469. "p_dx_AD": np.nan,
  470. "dof": np.nan,
  471. "n_used": np.nan,
  472. "model_covars": ",".join(X.columns.tolist()),
  473. "error": str(ex),
  474. })
  475. out = pd.DataFrame(rows)
  476. out["q_dx_AD"] = bh_fdr(out["p_dx_AD"].to_numpy(dtype=np.float64))
  477. out["sig_fdr"] = out["q_dx_AD"].apply(lambda x: significance_flag(np.nan, x, alpha=fdr_alpha))
  478. return out
  479. noN = _run_one(include_nodes=False, model_name="No_N")
  480. withN = _run_one(include_nodes=True, model_name="With_N")
  481. wide = noN.merge(
  482. withN,
  483. on=["analysis", "metric", "pretty_metric"],
  484. how="outer",
  485. suffixes=("_NoN", "_WithN")
  486. )
  487. wide["delta_beta"] = wide["beta_dx_AD_WithN"] - wide["beta_dx_AD_NoN"]
  488. wide["delta_p"] = wide["p_dx_AD_WithN"] - wide["p_dx_AD_NoN"]
  489. wide["delta_q"] = wide["q_dx_AD_WithN"] - wide["q_dx_AD_NoN"]
  490. def _change(row):
  491. a = row.get("sig_fdr_NoN", "NA")
  492. b = row.get("sig_fdr_WithN", "NA")
  493. if a == "Yes" and b == "Yes":
  494. return "Stable significant"
  495. if a == "Yes" and b == "No":
  496. return "Lost after N adjustment"
  497. if a == "No" and b == "Yes":
  498. return "Gained after N adjustment"
  499. if a == "No" and b == "No":
  500. return "Stable non-significant"
  501. return "NA"
  502. wide["significance_change"] = wide.apply(_change, axis=1)
  503. wide = wide.sort_values(["analysis", "p_dx_AD_NoN", "p_dx_AD_WithN"], na_position="last").reset_index(drop=True)
  504. return wide
  505. def make_pretty_table(df: pd.DataFrame) -> pd.DataFrame:
  506. out = df.copy()
  507. out["beta (No N)"] = out["beta_dx_AD_NoN"].map(lambda x: fmt_num(x, 4))
  508. out["p (No N)"] = out["p_dx_AD_NoN"].map(fmt_p)
  509. out["q (No N)"] = out["q_dx_AD_NoN"].map(fmt_p)
  510. out["beta (With N)"] = out["beta_dx_AD_WithN"].map(lambda x: fmt_num(x, 4))
  511. out["p (With N)"] = out["p_dx_AD_WithN"].map(fmt_p)
  512. out["q (With N)"] = out["q_dx_AD_WithN"].map(fmt_p)
  513. out["Δbeta"] = out["delta_beta"].map(lambda x: fmt_num(x, 4))
  514. out["Δq"] = out["delta_q"].map(lambda x: fmt_num(x, 4))
  515. keep_cols = [
  516. "analysis",
  517. "metric",
  518. "pretty_metric",
  519. "beta (No N)",
  520. "p (No N)",
  521. "q (No N)",
  522. "beta (With N)",
  523. "p (With N)",
  524. "q (With N)",
  525. "Δbeta",
  526. "Δq",
  527. "sig_fdr_NoN",
  528. "sig_fdr_WithN",
  529. "significance_change",
  530. "model_covars_NoN",
  531. "model_covars_WithN",
  532. ]
  533. rename_map = {
  534. "analysis": "Analysis",
  535. "metric": "Metric_code",
  536. "pretty_metric": "Metric",
  537. "sig_fdr_NoN": "FDR sig (No N)",
  538. "sig_fdr_WithN": "FDR sig (With N)",
  539. "model_covars_NoN": "Covariates (No N)",
  540. "model_covars_WithN": "Covariates (With N)",
  541. }
  542. return out[keep_cols].rename(columns=rename_map)
  543. # =========================================================
  544. # Subject-level extraction
  545. # =========================================================
  546. def load_subject_level_tables(entries, threshold_mode, top_percent, abs_thr, n_random, seed):
  547. global_rows = []
  548. local_rows = []
  549. for e in entries:
  550. label = int(e.get("label"))
  551. if label not in (2, 3):
  552. continue
  553. sid = str(e.get("subject_id"))
  554. site = str(e.get("site", ""))
  555. age = _safe_float(e.get("age", np.nan))
  556. sex = _safe_float(e.get("gender", np.nan))
  557. mat = e.get("matrix", None)
  558. if mat is None:
  559. continue
  560. G = build_graph(mat, threshold_mode, top_percent, abs_thr)
  561. base_info = {
  562. "subject_id": sid,
  563. "site": site,
  564. "label": label,
  565. "label_name": LABEL_NAME[label],
  566. "dx_AD": 1 if label == 2 else 0,
  567. "age": age,
  568. "sex": sex,
  569. }
  570. gmet = compute_global_metrics(G, n_random=n_random, seed=seed)
  571. lmet = compute_local_summary_metrics(G)
  572. global_rows.append({**base_info, **gmet})
  573. local_rows.append({**base_info, **lmet})
  574. global_df = pd.DataFrame(global_rows)
  575. local_df = pd.DataFrame(local_rows)
  576. if global_df.empty or local_df.empty:
  577. raise RuntimeError("No AD/LBD subjects found in input.")
  578. return global_df, local_df
  579. def merge_optional_covars(df: pd.DataFrame, covar_file: str | None, covar_cols: list[str]) -> pd.DataFrame:
  580. if covar_file is None:
  581. return df
  582. covp = Path(covar_file)
  583. if not covp.exists():
  584. raise FileNotFoundError(f"covar-file not found: {covp}")
  585. if covp.suffix.lower() == ".csv":
  586. cov = pd.read_csv(covp)
  587. else:
  588. cov = pd.read_csv(covp, sep="\t")
  589. if "subject_id" not in cov.columns:
  590. raise ValueError("covar-file must contain subject_id column.")
  591. keep = ["subject_id"] + [c for c in covar_cols if c in cov.columns]
  592. return df.merge(cov[keep], on="subject_id", how="left")
  593. # =========================================================
  594. # CLI
  595. # =========================================================
  596. def parse_args():
  597. ap = argparse.ArgumentParser("Compare GLM p/q with and without node-number adjustment")
  598. ap.add_argument("--in-npy", required=True, help="all_subjects.npy")
  599. ap.add_argument("--outdir", default="compare_node_effect_out")
  600. ap.add_argument("--threshold-mode", choices=["mst_top", "top", "abs", "none"], default="mst_top")
  601. ap.add_argument("--top-percent", type=float, default=10.0)
  602. ap.add_argument("--abs-thr", type=float, default=None)
  603. ap.add_argument("--n-random", type=int, default=20)
  604. ap.add_argument("--seed", type=int, default=0)
  605. ap.add_argument("--covar-file", default=None, help="Optional TSV/CSV with subject_id column")
  606. ap.add_argument("--covar-cols", nargs="*", default=["tiv"], help="Extra covariate columns, e.g. tiv")
  607. ap.add_argument("--fdr-alpha", type=float, default=0.05)
  608. ap.add_argument("--no-density", action="store_true",
  609. help="Do NOT include density as a covariate in the comparison models")
  610. return ap.parse_args()
  611. # =========================================================
  612. # Main
  613. # =========================================================
  614. def main():
  615. args = parse_args()
  616. outdir = Path(args.outdir)
  617. outdir.mkdir(parents=True, exist_ok=True)
  618. entries = np.load(args.in_npy, allow_pickle=True)
  619. global_df, local_df = load_subject_level_tables(
  620. entries=entries,
  621. threshold_mode=args.threshold_mode,
  622. top_percent=args.top_percent,
  623. abs_thr=args.abs_thr,
  624. n_random=args.n_random,
  625. seed=args.seed
  626. )
  627. global_df = merge_optional_covars(global_df, args.covar_file, args.covar_cols)
  628. local_df = merge_optional_covars(local_df, args.covar_file, args.covar_cols)
  629. global_metrics_path = outdir / "global_metrics_per_subject.tsv"
  630. local_metrics_path = outdir / "local_metrics_per_subject.tsv"
  631. global_df.to_csv(global_metrics_path, sep="\t", index=False)
  632. local_df.to_csv(local_metrics_path, sep="\t", index=False)
  633. global_metric_cols = [
  634. "global_efficiency",
  635. "char_path_length",
  636. "clustering",
  637. "transitivity",
  638. "modularity",
  639. "assortativity",
  640. "smallworld_sigma",
  641. "richclub_mean",
  642. "local_eff_mean",
  643. "local_eff_std",
  644. ]
  645. local_metric_cols = [
  646. "eff_q90", "eff_gini", "eff_std",
  647. "clust_q90", "clust_gini", "clust_std",
  648. "btw_q90", "btw_gini",
  649. "deg_gini", "deg_top10_mean_ratio",
  650. ]
  651. include_density = not args.no_density
  652. global_compare = run_glm_compare(
  653. df=global_df,
  654. metric_cols=global_metric_cols,
  655. extra_covars=args.covar_cols,
  656. include_density=include_density,
  657. fdr_alpha=args.fdr_alpha,
  658. analysis_name="global"
  659. )
  660. local_compare = run_glm_compare(
  661. df=local_df,
  662. metric_cols=local_metric_cols,
  663. extra_covars=args.covar_cols,
  664. include_density=include_density,
  665. fdr_alpha=args.fdr_alpha,
  666. analysis_name="local"
  667. )
  668. combined = pd.concat([global_compare, local_compare], axis=0, ignore_index=True)
  669. pretty = make_pretty_table(combined)
  670. global_compare_path = outdir / "global_glm_compare_node.tsv"
  671. local_compare_path = outdir / "local_glm_compare_node.tsv"
  672. combined_path = outdir / "combined_glm_compare_node.tsv"
  673. pretty_path = outdir / "combined_glm_compare_node_pretty.tsv"
  674. global_compare.to_csv(global_compare_path, sep="\t", index=False)
  675. local_compare.to_csv(local_compare_path, sep="\t", index=False)
  676. combined.to_csv(combined_path, sep="\t", index=False)
  677. pretty.to_csv(pretty_path, sep="\t", index=False)
  678. print("[OK] Saved:")
  679. print(" -", global_metrics_path)
  680. print(" -", local_metrics_path)
  681. print(" -", global_compare_path)
  682. print(" -", local_compare_path)
  683. print(" -", combined_path)
  684. print(" -", pretty_path)
  685. print()
  686. print("Settings:")
  687. print(" threshold_mode =", args.threshold_mode)
  688. print(" top_percent =", args.top_percent)
  689. print(" include_density=", include_density)
  690. print(" extra_covars =", args.covar_cols)
  691. print(" fdr_alpha =", args.fdr_alpha)
  692. if __name__ == "__main__":
  693. main()

compare_node_number_effect_full.py at commit dabd756, under MIT · at the source

Overview

Authors: Minheng Chen1, Chao Cao1, Tong Chen1, Dunia Alhamad2,3, Tianming Liu4, Li Su2,5, Dajiang Zhu1
ORCID iDs: Minheng Chen
  1. Department of Computer Science and Engineering, University of Texas at Arlington, Arlington, TX, United States
  2. Sheffield Institute for Translational Neuroscience, University of Sheffield, Sheffield, United Kingdom
  3. King Saud Bin Abdulaziz University for Health Sciences (KSAU-HS), Ministry of National Guard Health Affairs, Riyadh, Kingdom of Saudi Arabia
  4. School of Computing, University of Georgia, Athens, GA, United States
  5. Department of Psychiatry, University of Cambridge, Cambridge, United Kingdom
Journal: Imaging neuroscience (Cambridge, Mass.), volume 4, article IMAG.a.1322
Dates: received 12 April 2026; accepted 1 July 2026; published online 6 August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1162/imag.a.1322 · PMID 42569517 · PMCID PMC13449931 · OpenAlex W7167901362
Open access: diamond, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), Alzheimer's / dementia (population), systems (subfield)
Methods: Connectivity, Preprocessing, Graphs, Machine learning
Keywords: Alzheimer’s disease, Lewy body dementia, morphometric similarity networks, cortical folding, individualized brain networks
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: National Institute on Aging (R01AG075582); NINDS (R01NS128534); Sir Jules Thorn Charitable Trust (05/JTA); Alzheimer’s Research UK Senior Research Fellowship (ARUK-SRF2017B-1); Lewy Body Society (LBS/002/2019); Engineering and Physical Sciences Research Council of the UK (EP/K026992/1); Wellcome Trust (WT088441MA)
Citations: not cited yet (Europe PMC); 70 references in the paper

Abstract

Alzheimer’s disease (AD) and Lewy body dementia (LBD) are common neurodegenerative dementias with overlapping clinical presentations, making differential diagnosis challenging. While structural magnetic resonance imaging (MRI) has revealed characteristic regional atrophy patterns, regional morphometric measures alone may not fully capture distributed cortical alterations. Morphometric similarity networks (MSNs) offer a systems-level framework to characterize coordinated structural organization, but existing approaches typically rely on atlas-based parcellations that may obscure individual-specific cortical folding geometry. Here, we propose a fine-scale, folding-informed cortical similarity network framework based on automatically detected three-hinge gyral (3HG) landmarks. Using a thickness-constrained arealization strategy in native surface space, we define individualized cortical regions and construct subject-specific MSNs without cross-subject registration. We then investigate how network topology relates to landmark-defined node count and how these properties differ between AD and LBD. We find that several graph theoretical metrics, particularly global efficiency and characteristic path length, exhibit clear associations with the number of detected landmarks, indicating that topology in individualized networks is partly shaped by node availability. When accounting for landmark count, several apparent group differences in global topology are attenuated, whereas multiple heterogeneity-related metrics remain significant, indicating that node-count scaling substantially influences the interpretation of individualized network topology. Nevertheless, multivariate topological patterns remain informative for AD/LBD classification after residualizing for node count, and landmark count itself provides modest diagnostic information. These findings highlight node-count scaling as a key methodological consideration in individualized structural networks and suggest that folding-based MSNs capture disease-related variation in cortical network organization between AD and LBD.

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

Repository

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

m1nhengChen/Gyral-Folding-Network

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: dabd756dfc4af96b135ab8a16984595353277a37, 10 July 2026
Languages: Python (4)
Size: 10 files, 4 scripts
Software Heritage: not archived
Found in: “Data and Code Availability”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (4 files), pandas (4 files), Matplotlib (2 files), SciPy (2 files), NetworkX (1 file), scikit-learn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
6 files

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

Tracing map

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

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 4 scripts, each with its path and the digest of its content;
  • 9 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 and Code Availability

The data used in this study are not publicly available due to ethical and privacy restrictions but are available from the corresponding author upon reasonable request and with permission from the data providers. The code used to generate the results in this study is publicly available at https://github.com/m1nhengChen/Gyral-Folding-Network.

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

Recorded: type, language, journal, volume, pages, dates, 7 authors, 5 keywords, 7 funders, 70 references.

Cite

This paper

Chen, M., Cao, C., Chen, T., Alhamad, D., Liu, T., Su, L., & Zhu, D. (2026). Fine-scale individualized gyral folding-based cortical similarity networks reveal distinct organizational patterns in Alzheimer's disease and Lewy body dementia. Imaging neuroscience (Cambridge, Mass.), 4, IMAG.a.1322. https://doi.org/10.1162/imag.a.1322

BibTeX

@article{chen2026fine,
author = {Chen, Minheng and Cao, Chao and Chen, Tong and Alhamad, Dunia and Liu, Tianming and Su, Li and Zhu, Dajiang},
title = {{Fine-scale individualized gyral folding-based cortical similarity networks reveal distinct organizational patterns in Alzheimer's disease and Lewy body dementia}},
journal = {Imaging neuroscience (Cambridge, Mass.)},
year = {2026},
month = aug,
volume = {4},
pages = {IMAG.a.1322},
publisher = {MIT Press},
issn = {2837-6056},
doi = {10.1162/imag.a.1322},
url = {https://doi.org/10.1162/imag.a.1322},
pmid = {42569517},
pmcid = {PMC13449931}
}

RIS

TY - JOUR
AU - Chen, Minheng
AU - Cao, Chao
AU - Chen, Tong
AU - Alhamad, Dunia
AU - Liu, Tianming
AU - Su, Li
AU - Zhu, Dajiang
TI - Fine-scale individualized gyral folding-based cortical similarity networks reveal distinct organizational patterns in Alzheimer's disease and Lewy body dementia
T2 - Imaging neuroscience (Cambridge, Mass.)
J2 - Imaging Neurosci (Camb)
PY - 2026
DA - 2026/08/06
VL - 4
SP - IMAG.a.1322
SN - 2837-6056
PB - MIT Press
DO - 10.1162/imag.a.1322
UR - https://doi.org/10.1162/imag.a.1322
LA - en
ER -

CSL-JSON

{
"id": "10.1162/imag.a.1322",
"type": "article-journal",
"title": "Fine-scale individualized gyral folding-based cortical similarity networks reveal distinct organizational patterns in Alzheimer's disease and Lewy body dementia",
"container-title": "Imaging neuroscience (Cambridge, Mass.)",
"author": [
{
"family": "Chen",
"given": "Minheng"
},
{
"family": "Cao",
"given": "Chao"
},
{
"family": "Chen",
"given": "Tong"
},
{
"family": "Alhamad",
"given": "Dunia"
},
{
"family": "Liu",
"given": "Tianming"
},
{
"family": "Su",
"given": "Li"
},
{
"family": "Zhu",
"given": "Dajiang"
}
],
"container-title-short": "Imaging Neurosci (Camb)",
"volume": "4",
"page": "IMAG.a.1322",
"DOI": "10.1162/imag.a.1322",
"PMID": "42569517",
"PMCID": "PMC13449931",
"ISSN": "2837-6056",
"publisher": "MIT Press",
"URL": "https://doi.org/10.1162/imag.a.1322",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
6
]
]
}
}

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.1038/s42003-026-09956-6 [code]
Linking changes in sulcal morphometry to cognitive development from childhood to adolescence.
Journal: Communications biology
In common: statsmodels, scikit-learn, pandas, 3 other tools, structural MRI / diffusion, 6 references
[2] doi:10.1038/s42003-026-10276-y [code]
The cellular correlates and adolescent reorganisation of cortical myelination networks in the common marmoset.
Journal: Communications biology
In common: statsmodels, scikit-learn, pandas, 3 other tools, structural MRI / diffusion, 6 references
[3] doi:10.1038/s41467-026-74153-2 [code]
Regional, functional and transcriptomic decoding of multidimensional brain structure alterations in obsessive-compulsive disorder.
Journal: Nature communications
In common: statsmodels, scikit-learn, pandas, 3 other tools, 6 references
[4] doi:10.1038/s41467-026-73714-9 [code]
The genetic architecture of cortical similarity networks.
Journal: Nature communications
In common: pandas, SciPy, Matplotlib, 1 other tool, structural MRI / diffusion, 6 references
[5] doi:10.1162/netn.a.553 [code]
MIND versus MSN: A systematic evaluation of test-retest reliability and age sensitivity for T1-weighted structural similarity networks.
Journal: Network neuroscience (Cambridge, Mass.)
In common: structural MRI / diffusion, 8 references
[6] doi:10.1162/imag.a.1208 [code]
Brain network analysis in Alzheimer's disease and mild cognitive impairment using high-density diffuse optical tomography.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: NetworkX, statsmodels, scikit-learn, 4 other tools, systems, Alzheimer's / dementia, 2 references
[7] doi:10.1162/imag.a.1325 [code]
Decoding everyday levels of musical training from subcortical white-matter architecture.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: statsmodels, pandas, SciPy, 2 other tools, systems, structural MRI / diffusion, 4 references
[8] doi:10.1186/s12916-026-04903-y [code]
Structural connectome architecture and biological vulnerability shape cortical atrophy in cocaine use disorder.
Journal: BMC medicine
In common: statsmodels, scikit-learn, pandas, 3 other tools, structural MRI / diffusion, 3 references
[9] doi:10.1162/netn.a.546 [code]
Cortical similarity networks in the rat brain: Postnatal development and sensitivity to early life stress.
Journal: Network neuroscience (Cambridge, Mass.)
In common: pandas, SciPy, NumPy, structural MRI / diffusion, 5 references
[10] doi:10.1186/s40708-026-00316-y [code]
Generalizable and explainable deep learning for brain MRI: a multi-cohort evaluation of 3D architectures for age and sex prediction.
Journal: Brain informatics
In common: scikit-learn, pandas, SciPy, 2 other tools, structural MRI / diffusion, 3 references

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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