OSCR

Spatial analysis of paraneoplastic cerebellar degeneration in ovarian cancer with anti-Yo syndrome and SCA1.

Code ↔ Paper

12 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 12 matches
  1. [1] § Methods › Analysis ↔ notebooks/3d_thickness_analysis.ipynb, lines 604–650 · score 0.93 · Lobule III, Lobule VIIIA, Lobule IX, Lobule VIIB, Lobule VIIIB, Lobule Crus
  2. [2] § Methods › Analysis ↔ notebooks/network_analysis.ipynb, lines 397–441 · score 0.92 · Lobule III, Lobule VIIIA, Lobule IX, Lobule VIIB, Lobule VIIIB, Lobule Crus
  3. [3] § Methods › Analysis ↔ notebooks/group_classification.ipynb, lines 409–515 · score 0.88 · Random Forest classifier, class weights, residualized feature, age residualized, CV, stratified
  4. [4] § Methods › Analysis ↔ notebooks/network_analysis.ipynb, lines 915–957 · score 0.79 · absolute thresholds, proportional thresholding, NetworkX, strongest, network metric, density
  5. [5] § Methods › Analysis ↔ notebooks/group_differences.ipynb, lines 358–428 · score 0.76 · bootstrap cluster stability, silhouette scores, Adjusted Rand, ARI, patient
  6. [6] § Results › Classification of patient groups ↔ notebooks/group_classification.ipynb, lines 409–515 · score 0.74 · random forest classifier, age residualized features, class weights, Permutation, shuffles, fit
  7. [7] § Results › Graph-based metrics ↔ notebooks/network_analysis.ipynb, lines 865–913 · score 0.73 · correlation thresholds, global efficiency, network metric, node strength, global clustering, density
  8. [8] § Results › Graph-based metrics ↔ notebooks/network_analysis.ipynb, lines 915–957 · score 0.71 · proportional thresholding, global efficiency, node strength, Global clustering, absolute, edge
  9. [9] § Results › Cortical thickness analysis ↔ notebooks/group_differences.ipynb, lines 358–428 · score 0.58 · silhouette score, Adjusted Rand, Cluster stability, patients, PCD
  10. [10] § Results › Classification of patient groups ↔ notebooks/group_classification.ipynb, lines 125–173 · score 0.57 · random forest classifier, ROC curves, folds, AUCs, residualized
  11. [11] § Methods › Analysis ↔ notebooks/network_analysis.ipynb, lines 518–579 · score 0.57 · Node strength, clustering coefficient, shortest, efficiency, edge, weights
  12. [12] § Methods › Analysis ↔ notebooks/network_analysis.ipynb, lines 518–579 · score 0.54 · NetworkX, graph metrics, Connectivity

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 · 1,059 lines · 34 KB · no license · 6 matches

  1. # %%
  2. import nibabel as nib
  3. import os
  4. import numpy as np
  5. import pandas as pd
  6. from sklearn.linear_model import LinearRegression
  7. from statsmodels.api import OLS, add_constant
  8. from statsmodels.stats.multitest import multipletests
  9. import matplotlib.pyplot as plt
  10. from matplotlib.colors import ListedColormap
  11. from nilearn.plotting import plot_stat_map
  12. from nilearn.image import load_img
  13. import scipy.stats as stats
  14. from nilearn import plotting, datasets
  15. from nilearn.plotting import plot_anat, plot_stat_map, view_img
  16. from scipy.ndimage import center_of_mass
  17. from nilearn.image import resample_to_img
  18. from scipy import stats
  19. from netplotbrain import plot as netplot
  20. plt.rcParams.update({
  21. 'font.size': 12,
  22. 'axes.titlesize': 14,
  23. 'axes.labelsize': 12,
  24. 'xtick.labelsize': 10,
  25. 'ytick.labelsize': 10,
  26. 'legend.fontsize': 10,
  27. 'figure.dpi': 100,
  28. 'savefig.dpi': 900,
  29. 'font.family': 'sans-serif',
  30. 'font.sans-serif': ['Arial', 'DejaVu Sans'],
  31. 'pdf.fonttype': 42,
  32. })
  33. # %%
  34. def compute_barycenters(mask_path, exclude_regions, target_affine):
  35. """
  36. Compute barycenters for regions in a given NIfTI mask, aligned to the target atlas space.
  37. """
  38. mask_img = nib.load(mask_path)
  39. mask_data = mask_img.get_fdata()
  40. unique_regions = np.unique(mask_data)[1:] # Exclude background (0)
  41. coords = []
  42. included_regions = []
  43. for region in unique_regions:
  44. if region not in exclude_regions:
  45. region_mask = (mask_data == region)
  46. barycenter = center_of_mass(region_mask)
  47. coords.append(barycenter)
  48. included_regions.append(region)
  49. # Transform barycenters to target atlas space
  50. coords = np.array(coords)
  51. transformed_coords = nib.affines.apply_affine(target_affine, coords)
  52. return transformed_coords, included_regions
  53. def extract_region_thickness(data_path, subject_files, mask_path, included_regions):
  54. """
  55. Extract region-wise cortical thickness data using a custom mask.
  56. """
  57. mask_img = nib.load(mask_path)
  58. mask_data = mask_img.get_fdata()
  59. region_thickness = []
  60. for path in subject_files:
  61. subject_img = nib.load(data_path + path)
  62. resampled_img = resample_to_img(subject_img, mask_img, interpolation="nearest")
  63. resampled_data = resampled_img.get_fdata()
  64. subject_region_means = [
  65. np.median(resampled_data[mask_data == region]) for region in included_regions
  66. ]
  67. region_thickness.append(subject_region_means)
  68. return np.array(region_thickness)
  69. def compute_structural_covariance(data):
  70. """
  71. Compute structural covariance as pairwise correlations between nodes.
  72. """
  73. n_regions = data.shape[1]
  74. corr_matrix = np.zeros((n_regions, n_regions))
  75. for i in range(n_regions):
  76. for j in range(i, n_regions):
  77. if np.std(data[:, i]) == 0 or np.std(data[:, j]) == 0:
  78. corr_matrix[i, j] = corr_matrix[j, i] = 0 # Avoid divide by zero
  79. else:
  80. corr_matrix[i, j] = corr_matrix[j, i] = np.corrcoef(data[:, i], data[:, j])[0, 1]
  81. return corr_matrix
  82. def plot_network_on_brain(coords, adj_matrix, title, filename, template=None):
  83. """
  84. Plot the network on the brain using NetPlotBrain.
  85. """
  86. edges = []
  87. for i in range(adj_matrix.shape[0]):
  88. for j in range(i + 1, adj_matrix.shape[1]):
  89. if adj_matrix[i, j] != 0:
  90. edges.append({"i": i, "j": j, "weight": adj_matrix[i, j]})
  91. edges_df = pd.DataFrame(edges)
  92. nodes_df = pd.DataFrame(coords, columns=["x", "y", "z"])
  93. netplot(
  94. template="MNI152NLin2009cAsym",
  95. nodes=nodes_df,
  96. edges=edges_df,
  97. node_size=5,
  98. edge_color="weight",
  99. edge_cmap="coolwarm",
  100. margin=0.05,
  101. #view="preset-6",
  102. view = 'LRSP',
  103. savename=filename,
  104. )
  105. # %%
  106. # Paths to thickness data for two groups
  107. group1_paths = [
  108. "IXI158.nii", "IXI204.nii", "IXI237.nii", "IXI251.nii", "IXI288.nii",
  109. "IXI383.nii", "IXI384.nii", "IXI399.nii", "IXI420.nii", "IXI433.nii",
  110. "IXI462.nii", "IXI464.nii", "IXI476.nii", "IXI491.nii", "IXI498.nii",
  111. "IXI518.nii", "IXI538.nii"
  112. ]
  113. group2_paths = [
  114. "Patient1.nii", "Patient2.nii", "Patient3.nii", "Patient4.nii",
  115. "Patient5.nii", "Patient6.nii", "Patient8.nii", "Patient9.nii",
  116. "Patient10.nii", "Patient11.nii", "Patient12.nii"
  117. ]
  118. group3_paths = ["SCA013.nii", "SCA029.nii", "SCA033.nii", "SCA039.nii",
  119. "SCA068.nii", "SCA069.nii", "SCA076.nii", "SCA077.nii",
  120. "SCA083.nii", "SCA084.nii", "SCA087.nii", "SCA090.nii",
  121. "SCA093.nii", "SCA094.nii", "SCA095.nii", "SCA096.nii",
  122. "SCA097.nii"
  123. ]
  124. data_path="../data/normal_yo_SCA/"
  125. # %%
  126. # Load mask and compute barycenters
  127. mask_path = "../data/mni_structures_job1687583.nii"
  128. mask_img = nib.load(mask_path)
  129. atlas = datasets.fetch_atlas_schaefer_2018()
  130. target_affine = mask_img.affine
  131. #target_affine = nib.load(atlas["maps"]).affine # Get affine from atlas
  132. exclude_regions = [13, 113] # Exclude regions
  133. coords, included_regions = compute_barycenters(mask_path, exclude_regions, target_affine)
  134. # %%
  135. # Extract region-wise cortical thickness
  136. group1_data = extract_region_thickness(data_path, group1_paths, mask_path, included_regions)
  137. group2_data = extract_region_thickness(data_path, group2_paths, mask_path, included_regions)
  138. group3_data = extract_region_thickness(data_path, group3_paths, mask_path, included_regions)
  139. # %%
  140. np.save('../data/group1_data.npy', group1_data)
  141. np.save('../data/group2_data.npy', group2_data)
  142. np.save('../data/group3_data.npy', group3_data)
  143. # Load the array
  144. group1_data = np.load('../data/group1_data.npy')
  145. group2_data = np.load('../data/group2_data.npy')
  146. group3_data = np.load('../data/group3_data.npy')
  147. # %%
  148. # Compute structural covariance matrices
  149. corr_matrix_group1 = compute_structural_covariance(group1_data)
  150. corr_matrix_group2 = compute_structural_covariance(group2_data)
  151. corr_matrix_group3 = compute_structural_covariance(group3_data)
  152. # %%
  153. threshold = 0.0
  154. adj_matrix_group1 = np.where(np.abs(corr_matrix_group1) > threshold, corr_matrix_group1, 0)
  155. adj_matrix_group2 = np.where(np.abs(corr_matrix_group2) > threshold, corr_matrix_group2, 0)
  156. adj_matrix_group3 = np.where(np.abs(corr_matrix_group3) > threshold, corr_matrix_group3, 0)
  157. # %%
  158. # Compute difference matrix
  159. diff_matrix = adj_matrix_group2 - adj_matrix_group1
  160. diff_matrix_13 = adj_matrix_group3 - adj_matrix_group1
  161. diff_matrix_23 = adj_matrix_group2 - adj_matrix_group3
  162. # %%
  163. from scipy.stats import ttest_ind
  164. # %%
  165. # Perform pairwise t-tests with FDR correction
  166. n_nodes = group1_data.shape[1]
  167. p_values = []
  168. indices = []
  169. # Collect p-values
  170. for i in range(n_nodes):
  171. for j in range(i + 1, n_nodes):
  172. t_stat, p_value = ttest_ind(group1_data[:, i], group2_data[:, j]) # Pairwise comparison
  173. p_values.append(p_value)
  174. indices.append((i, j))
  175. # Apply FDR correction
  176. rejected, p_values_corrected, _, _ = multipletests(p_values, alpha=0.05, method="fdr_bh")
  177. # Update significance matrix
  178. sig_matrix = np.zeros((n_nodes, n_nodes))
  179. for idx, (i, j) in enumerate(indices):
  180. if rejected[idx]:
  181. sig_matrix[i, j] = sig_matrix[j, i] = 1 - p_values_corrected[idx]
  182. # %%
  183. # Perform pairwise t-tests with FDR correction
  184. n_nodes = group1_data.shape[1]
  185. p_values = []
  186. indices = []
  187. # Collect p-values
  188. for i in range(n_nodes):
  189. for j in range(i + 1, n_nodes):
  190. t_stat, p_value = ttest_ind(group1_data[:, i], group3_data[:, j])
  191. p_values.append(p_value)
  192. indices.append((i, j))
  193. # Apply FDR correction
  194. rejected, p_values_corrected, _, _ = multipletests(p_values, alpha=0.05, method="fdr_bh")
  195. # Update significance matrix
  196. sig_matrix_13 = np.zeros((n_nodes, n_nodes))
  197. for idx, (i, j) in enumerate(indices):
  198. if rejected[idx]:
  199. sig_matrix_13[i, j] = sig_matrix_13[j, i] = 1 - p_values_corrected[idx]
  200. # %%
  201. # Perform pairwise t-tests with FDR correction
  202. n_nodes = group1_data.shape[1]
  203. p_values = []
  204. indices = []
  205. # Collect p-values
  206. for i in range(n_nodes):
  207. for j in range(i + 1, n_nodes):
  208. t_stat, p_value = ttest_ind(group2_data[:, i], group3_data[:, j])
  209. p_values.append(p_value)
  210. indices.append((i, j))
  211. # Apply FDR correction
  212. rejected, p_values_corrected, _, _ = multipletests(p_values, alpha=0.05, method="fdr_bh")
  213. # Update significance matrix
  214. sig_matrix_23 = np.zeros((n_nodes, n_nodes))
  215. for idx, (i, j) in enumerate(indices):
  216. if rejected[idx]:
  217. sig_matrix_23[i, j] = sig_matrix_23[j, i] = 1 - p_values_corrected[idx]
  218. # %%
  219. def plot_combined_adjacency_matrices(adj_group1, adj_group2, diff_matrix, sig_matrix, filename, groupname_1, groupname_2):
  220. """
  221. Plot adjacency matrices for two groups, their difference, and the significant clusters in a single layout.
  222. """
  223. fig, ax = plt.subplots(1, 4, figsize=(24, 6))
  224. cmap = "RdBu_r"
  225. vmin, vmax = -1, 1 # Adjusted for correlation range
  226. ax[0].imshow(adj_group1, cmap=cmap, vmin=vmin, vmax=vmax)
  227. ax[0].set_title(groupname_1)
  228. ax[0].set_xlabel("Nodes")
  229. ax[0].set_ylabel("Nodes")
  230. ax[1].imshow(adj_group2, cmap=cmap, vmin=vmin, vmax=vmax)
  231. ax[1].set_title(groupname_2)
  232. ax[1].set_xlabel("Nodes")
  233. ax[2].imshow(diff_matrix, cmap="RdBu_r", vmin=-1, vmax=1)
  234. ax[2].set_title("Difference Matrix")
  235. ax[2].set_xlabel("Nodes")
  236. ax[3].imshow(sig_matrix, cmap="binary", interpolation="nearest")
  237. ax[3].set_title("Significant Differences")
  238. ax[3].set_xlabel("Nodes")
  239. plt.tight_layout()
  240. plt.savefig(filename, bbox_inches='tight', dpi=300)
  241. plt.savefig(filename.replace('.svg', '.png'), bbox_inches='tight', dpi=600)
  242. plt.show()
  243. # %%
  244. labels = 4*["anterior"]+8*['posterior']+4*["anterior"]+8*['posterior']
  245. # %%
  246. # Create combined adjacency matrix plot
  247. plot_combined_adjacency_matrices(
  248. adj_matrix_group1, adj_matrix_group2, diff_matrix, sig_matrix,
  249. "../results/Combined_Adjacency_Matrices_with_Significance_12.png",
  250. "Healthy Group (Median)", "anti-Yo PCD Group (Median)",
  251. )
  252. # %%
  253. from graspologic.plot import heatmap
  254. def plot_combined_adjacency_matrices(
  255. adj_group1, adj_group2, diff_matrix, sig_matrix, filename, groupname_1, groupname_2, labels
  256. ):
  257. """
  258. Plot adjacency matrices for two groups, their difference, and the significant clusters in a single layout using graspologic's heatmap.
  259. """
  260. fig, axes = plt.subplots(1, 4, figsize=(30, 10))
  261. font_scale = 1.5
  262. hier_label_fontsize = 20
  263. # Group 1
  264. heatmap(
  265. adj_group1,
  266. ax=axes[0],
  267. inner_hier_labels=labels,
  268. sort_nodes=True,
  269. cbar=False,
  270. title=groupname_1,
  271. font_scale=font_scale,
  272. hier_label_fontsize=hier_label_fontsize
  273. )
  274. axes[0].set_xlabel("Nodes")
  275. axes[0].set_ylabel("Nodes")
  276. # Group 2
  277. heatmap(
  278. adj_group2,
  279. ax=axes[1],
  280. inner_hier_labels=labels,
  281. sort_nodes=True,
  282. cbar=False,
  283. title=groupname_2,
  284. font_scale=font_scale,
  285. hier_label_fontsize=hier_label_fontsize
  286. )
  287. axes[1].set_xlabel("Nodes")
  288. # Difference Matrix
  289. heatmap(
  290. diff_matrix,
  291. ax=axes[2],
  292. inner_hier_labels=labels,
  293. sort_nodes=True,
  294. cbar=False,
  295. title="Difference Matrix",
  296. font_scale=font_scale,
  297. hier_label_fontsize=hier_label_fontsize
  298. )
  299. axes[2].set_xlabel("Nodes")
  300. # Significant Differences
  301. heatmap(
  302. sig_matrix,
  303. ax=axes[3],
  304. inner_hier_labels=labels,
  305. sort_nodes=True,
  306. cbar=False,
  307. title="Significant Differences",
  308. font_scale=font_scale,
  309. hier_label_fontsize=hier_label_fontsize
  310. )
  311. axes[3].set_xlabel("Nodes")
  312. plt.tight_layout()
  313. plt.savefig(filename, bbox_inches='tight', dpi=300)
  314. plt.savefig(filename.replace('.svg', '.png'), bbox_inches='tight', dpi=600)
  315. plt.show()
  316. # %%
  317. # Create combined adjacency matrix plot
  318. plot_combined_adjacency_matrices(
  319. adj_matrix_group1, adj_matrix_group2, diff_matrix, sig_matrix,
  320. "../results/Combined_Adjacency_Matrices_with_Significance_12.svg",
  321. "Control", "PCD",
  322. labels=labels
  323. )
  324. # %%
  325. # Create combined adjacency matrix plot
  326. plot_combined_adjacency_matrices(
  327. adj_matrix_group1, adj_matrix_group3, diff_matrix_13, sig_matrix_13,
  328. "../results/Combined_Adjacency_Matrices_with_Significance_13.svg", "Control", "SCA",
  329. labels
  330. )
  331. # %%
  332. # Create combined adjacency matrix plot
  333. plot_combined_adjacency_matrices(
  334. adj_matrix_group3, adj_matrix_group2, diff_matrix_23, sig_matrix_23, "../results/Combined_Adjacency_Matrices_with_Significance_23.svg",
  335. "SCA", "PCD",
  336. labels
  337. )
  338. # %% [markdown]
  339. # ### Network
  340. # %% [markdown]
  341. # ### Network analysis
  342. # %%
  343. import networkx as nx
  344. import matplotlib.patches as mpatches
  345. # Color mapping for labels
  346. color_dict = {
  347. 'posterior': '#aff8df',
  348. 'anterior': '#ffcbc1'
  349. }
  350. # Define the mapping for node labels
  351. lobule_mapping = {
  352. 0: 'Lobule I-II',
  353. 1: 'Lobule III',
  354. 2: 'Lobule IV',
  355. 3: 'Lobule V',
  356. 4: 'Lobule VI',
  357. 5: 'Lobule Crus I',
  358. 6: 'Lobule Crus II',
  359. 7: 'Lobule VIIB',
  360. 8: 'Lobule VIIIA',
  361. 9: 'Lobule VIIIB',
  362. 10: 'Lobule IX',
  363. 11: 'Lobule X',
  364. 12: 'Lobule I-II (R)',
  365. 13: 'Lobule III (R)',
  366. 14: 'Lobule IV (R)',
  367. 15: 'Lobule V (R)',
  368. 16: 'Lobule VI (R)',
  369. 17: 'Lobule Crus I (R)',
  370. 18: 'Lobule Crus II (R)',
  371. 19: 'Lobule VIIB (R)',
  372. 20: 'Lobule VIIIA (R)',
  373. 21: 'Lobule VIIIB (R)',
  374. 22: 'Lobule IX (R)',
  375. 23: 'Lobule X (R)'
  376. }
  377. collabels = 4*["anterior"]+8*['posterior']+4*["anterior"]+8*['posterior']
  378. # %%
  379. coords_2d = coords[:,[0,2]]
  380. def create_graph(adj_matrix, lobule_mapping, collabels):
  381. A = adj_matrix.copy()
  382. np.fill_diagonal(A, 0)
  383. g = nx.from_numpy_array(A)
  384. for i in range(len(g.nodes)):
  385. g.nodes[i]['lobule'] = lobule_mapping[i]
  386. g.nodes[i]['lobulenr'] = list(lobule_mapping.keys())[i]
  387. g.nodes[i]['color'] = collabels[i]
  388. return g
  389. # List of adjacency matrices
  390. threshold = 0.5
  391. adj_matrix_group1 = np.where(np.abs(corr_matrix_group1) > threshold, corr_matrix_group1, 0)
  392. adj_matrix_group2 = np.where(np.abs(corr_matrix_group2) > threshold, corr_matrix_group2, 0)
  393. adj_matrix_group3 = np.where(np.abs(corr_matrix_group3) > threshold, corr_matrix_group3, 0)
  394. adj_matrices = [adj_matrix_group1, adj_matrix_group2, adj_matrix_group3]
  395. titles = ["Control", "PCD", "SCA"]
  396. # Create the plot
  397. fig, axes = plt.subplots(1, 3, figsize=(15, 5))
  398. for idx, (adj_matrix, ax) in enumerate(zip(adj_matrices, axes)):
  399. g = create_graph(adj_matrix, lobule_mapping, collabels)
  400. # Get node colors directly from the graph
  401. node_colors = [color_dict[g.nodes[n]['color']] for n in g.nodes()]
  402. nx.draw_networkx(
  403. g,
  404. pos=coords_2d,
  405. labels=nx.get_node_attributes(g, 'lobulenr'),
  406. node_size=200,
  407. node_color=node_colors,
  408. with_labels=True,
  409. font_weight='bold',
  410. font_size=6,
  411. width=0.6,
  412. ax=ax
  413. )
  414. ax.set_title(titles[idx])
  415. ax.axis('off')
  416. # Create the main legend for posterior and anterior
  417. legend_tiles = [
  418. mpatches.Patch(color="#aff8df", label="posterior"),
  419. mpatches.Patch(color="#ffcbc1", label="anterior"),
  420. ]
  421. # Add the main legend to the first subplot
  422. axes[0].legend(handles=legend_tiles, loc="lower right")
  423. # Create the lobule legend
  424. lobule_legend_labels = [f"{key}: {value}" for key, value in lobule_mapping.items()]
  425. lobule_legend_patches = [mpatches.Patch(color='white', label=label) for label in lobule_legend_labels]
  426. # Add the lobule legend outside the plot
  427. fig.legend(handles=lobule_legend_patches, loc="upper left", bbox_to_anchor=(1, 0.95), fontsize=9)
  428. # Adjust layout to fit legends
  429. plt.tight_layout()
  430. plt.savefig('../results/network_graph.svg', bbox_inches='tight', dpi=300)
  431. plt.savefig('../results/network_graph.png', bbox_inches='tight', dpi=600)
  432. plt.show()
  433. # %%
  434. g1 = create_graph(adj_matrix_group1, lobule_mapping, collabels)
  435. g2 = create_graph(adj_matrix_group2, lobule_mapping, collabels)
  436. g3 = create_graph(adj_matrix_group3, lobule_mapping, collabels)
  437. # %%
  438. def calculate_graph_metrics(g):
  439. metrics = {}
  440. # Number of nodes and edges
  441. metrics['num_nodes'] = g.number_of_nodes()
  442. metrics['num_edges'] = g.number_of_edges()
  443. # Check if the graph is connected
  444. metrics['is_connected'] = nx.is_connected(g)
  445. metrics['num_components'] = nx.number_connected_components(g)
  446. # Global clustering coefficient
  447. metrics['global_clustering_coefficient'] = nx.average_clustering(g)
  448. # Global efficiency
  449. metrics['global_efficiency'] = nx.global_efficiency(g)
  450. # Small-worldness (if connected)
  451. if metrics['is_connected']:
  452. C = nx.average_clustering(g)
  453. L = nx.average_shortest_path_length(g)
  454. n = g.number_of_nodes()
  455. k = 2 * g.number_of_edges() / n
  456. C_rand = k / n
  457. L_rand = np.log(n) / np.log(k)
  458. metrics['small_worldness'] = (C / C_rand) / (L / L_rand)
  459. else:
  460. metrics['small_worldness'] = "Not applicable (disconnected graph)"
  461. # Node-level metrics
  462. metrics['node_strength'] = dict(g.degree(weight='weight'))
  463. metrics['betweenness_centrality'] = nx.betweenness_centrality(g)
  464. metrics['clustering_coefficient'] = nx.clustering(g)
  465. # Average shortest path length (for each component)
  466. component_path_lengths = []
  467. for component in nx.connected_components(g):
  468. subgraph = g.subgraph(component)
  469. if len(subgraph) > 1:
  470. component_path_lengths.append(nx.average_shortest_path_length(subgraph))
  471. metrics['avg_shortest_path_length'] = component_path_lengths
  472. # Participation coefficient (if the graph has communities)
  473. try:
  474. communities = list(nx.community.greedy_modularity_communities(g))
  475. metrics['participation_coefficient'] = nx.algorithms.participation_coefficient(g, communities)
  476. except:
  477. metrics['participation_coefficient'] = "Not calculated (requires community structure)"
  478. return metrics
  479. # Example usage
  480. # Assuming g is your NetworkX graph
  481. # g = nx.Graph() # Replace this with your actual graph
  482. # Calculate metrics
  483. metrics = calculate_graph_metrics(g)
  484. # Print results
  485. for metric, value in metrics.items():
  486. print(f"{metric}: {value}")
  487. # %%
  488. # Calculate metrics
  489. metrics1 = calculate_graph_metrics(g1)
  490. metrics2 = calculate_graph_metrics(g2)
  491. metrics3 = calculate_graph_metrics(g3)
  492. # %%
  493. def create_boxplot(data1, data2, data3, title, ax, name):
  494. df = pd.DataFrame({
  495. 'Control': data1,
  496. 'PCD': data2,
  497. 'SCA': data3
  498. })
  499. df_melted = df.melt(var_name='Group', value_name=name)
  500. custom_palette = {'PCD': 'cornflowerblue', 'Control': 'turquoise', 'SCA': 'orange'}
  501. sns.boxplot(
  502. x='Group', y=name, data=df_melted, palette=custom_palette, ax=ax,
  503. width=0.5, fliersize=0, linewidth=1.0, saturation=0.45,
  504. )
  505. sns.stripplot(
  506. x='Group', y=name, data=df_melted, palette=custom_palette, ax=ax,
  507. size=4.5, jitter=0.12, edgecolor='black', linewidth=0.4, alpha=0.9,
  508. )
  509. _, p12 = stats.ttest_ind(data1, data2, equal_var=False)
  510. _, p13 = stats.ttest_ind(data1, data3, equal_var=False)
  511. _, p23 = stats.ttest_ind(data2, data3, equal_var=False)
  512. y_max = df_melted[name].max()
  513. add_significance_bar(0, 1, y_max*1.05, p12, ax)
  514. add_significance_bar(1, 2, y_max*1.15, p23, ax)
  515. add_significance_bar(0, 2, y_max*1.25, p13, ax)
  516. ax.set_title(title, fontsize=14)
  517. ax.set_xlabel(None)
  518. ax.set_ylabel(None)
  519. ax.set_ylim(0, y_max*1.4)
  520. def format_pvalue(p_value):
  521. if p_value < 0.001:
  522. return "p < 0.001"
  523. elif p_value < 0.01:
  524. return f"p = {p_value:.3f}"
  525. else:
  526. return f"p = {p_value:.2f}"
  527. def add_significance_bar(start, end, height, p_value, ax):
  528. x1, x2 = start, end
  529. y, h = height, height * 0.05
  530. ax.plot([x1, x1, x2, x2], [y, y+h, y+h, y], lw=1.5, c='black')
  531. ax.text((x1+x2)*.5, y+h, format_pvalue(p_value), ha='center', va='bottom', fontsize=10)
  532. # Function to filter nodes based on color
  533. def filter_nodes(g, metrics, color, metric):
  534. return [metrics[metric][n] for n in g.nodes() if g.nodes[n]['color'] == color]
  535. # %%
  536. import seaborn as sns
  537. metric_to_plot = 'node_strength'
  538. # Create the main figure
  539. fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(8, 4))
  540. plt.subplots_adjust(wspace=0.08)
  541. # All nodes
  542. create_boxplot(
  543. list(metrics1[metric_to_plot].values()),
  544. list(metrics2[metric_to_plot].values()),
  545. list(metrics3[metric_to_plot].values()),
  546. 'All Nodes', ax1, 'Node Strength'
  547. )
  548. # Anterior nodes
  549. create_boxplot(
  550. filter_nodes(g1, metrics1, 'anterior', metric_to_plot),
  551. filter_nodes(g2, metrics2, 'anterior', metric_to_plot),
  552. filter_nodes(g3, metrics3, 'anterior', metric_to_plot),
  553. 'Anterior Nodes', ax2, 'Node Strength'
  554. )
  555. # Posterior nodes
  556. create_boxplot(
  557. filter_nodes(g1, metrics1, 'posterior', metric_to_plot),
  558. filter_nodes(g2, metrics2, 'posterior', metric_to_plot),
  559. filter_nodes(g3, metrics3, 'posterior', metric_to_plot),
  560. 'Posterior Nodes', ax3, 'Node Strength'
  561. )
  562. # Share a single y-axis across all subplots, displayed on the far right only
  563. global_ymax = max(ax.get_ylim()[1] for ax in (ax1, ax2, ax3))
  564. for ax in (ax1, ax2, ax3):
  565. ax.set_ylim(0, global_ymax)
  566. ax.spines['top'].set_visible(False)
  567. for ax in (ax1, ax2):
  568. ax.spines['left'].set_visible(False)
  569. ax.spines['right'].set_visible(False)
  570. ax.tick_params(axis='y', which='both', left=False, right=False, labelleft=False)
  571. ax.tick_params(axis='x', which='both', bottom=False, labelbottom=False)
  572. ax3.spines['left'].set_visible(False)
  573. ax3.spines['right'].set_visible(True)
  574. ax3.yaxis.tick_right()
  575. ax3.yaxis.set_label_position('right')
  576. ax3.set_ylabel('Node Strength', fontsize=14, labelpad=8)
  577. plt.tight_layout()
  578. plt.savefig('../results/node_strength.svg', bbox_inches='tight', dpi=300)
  579. plt.savefig('../results/node_strength.png', bbox_inches='tight', dpi=600)
  580. plt.show()
  581. # %%
  582. metric_to_plot = 'betweenness_centrality'
  583. fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(8, 4))
  584. plt.subplots_adjust(wspace=0.08)
  585. def normalize(x):
  586. return (x - np.min(x)) / (np.max(x) - np.min(x))
  587. create_boxplot(
  588. np.log1p(list(metrics1[metric_to_plot].values())),
  589. np.log1p(list(metrics2[metric_to_plot].values())),
  590. np.log1p(list(metrics3[metric_to_plot].values())),
  591. 'All Nodes', ax1, 'Log Betweenness Centrality'
  592. )
  593. create_boxplot(
  594. np.log1p(filter_nodes(g1, metrics1, 'anterior', metric_to_plot)),
  595. np.log1p(filter_nodes(g2, metrics2, 'anterior', metric_to_plot)),
  596. np.log1p(filter_nodes(g3, metrics3, 'anterior', metric_to_plot)),
  597. 'Anterior Nodes', ax2, 'Log Betweenness Centrality'
  598. )
  599. create_boxplot(
  600. np.log1p(filter_nodes(g1, metrics1, 'posterior', metric_to_plot)),
  601. np.log1p(filter_nodes(g2, metrics2, 'posterior', metric_to_plot)),
  602. np.log1p(filter_nodes(g3, metrics3, 'posterior', metric_to_plot)),
  603. 'Posterior Nodes', ax3, 'Log Betweenness Centrality'
  604. )
  605. # Share a single y-axis across all subplots, displayed on the far right only
  606. global_ymax = max(ax.get_ylim()[1] for ax in (ax1, ax2, ax3))
  607. for ax in (ax1, ax2, ax3):
  608. ax.set_ylim(0, global_ymax)
  609. ax.spines['top'].set_visible(False)
  610. for ax in (ax1, ax2):
  611. ax.spines['left'].set_visible(False)
  612. ax.spines['right'].set_visible(False)
  613. ax.tick_params(axis='y', which='both', left=False, right=False, labelleft=False)
  614. ax.tick_params(axis='x', which='both', bottom=False, labelbottom=False)
  615. ax3.spines['left'].set_visible(False)
  616. ax3.spines['right'].set_visible(True)
  617. ax3.yaxis.tick_right()
  618. ax3.yaxis.set_label_position('right')
  619. ax3.set_ylabel('Log Betweenness Centrality', fontsize=14, labelpad=8)
  620. plt.tight_layout()
  621. plt.savefig('../results/betweenness_centrality.svg', bbox_inches='tight', dpi=300)
  622. plt.savefig('../results/betweenness_centrality.png', bbox_inches='tight', dpi=600)
  623. plt.show()
  624. # %%
  625. metric_to_plot = 'clustering_coefficient'
  626. fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(8, 4))
  627. plt.subplots_adjust(wspace=0.08)
  628. create_boxplot(
  629. list(metrics1[metric_to_plot].values()),
  630. list(metrics2[metric_to_plot].values()),
  631. list(metrics3[metric_to_plot].values()),
  632. 'All Nodes', ax1, 'Clustering Coefficient'
  633. )
  634. create_boxplot(
  635. filter_nodes(g1, metrics1, 'anterior', metric_to_plot),
  636. filter_nodes(g2, metrics2, 'anterior', metric_to_plot),
  637. filter_nodes(g3, metrics3, 'anterior', metric_to_plot),
  638. 'Anterior Nodes', ax2, 'Clustering Coefficient'
  639. )
  640. create_boxplot(
  641. filter_nodes(g1, metrics1, 'posterior', metric_to_plot),
  642. filter_nodes(g2, metrics2, 'posterior', metric_to_plot),
  643. filter_nodes(g3, metrics3, 'posterior', metric_to_plot),
  644. 'Posterior Nodes', ax3, 'Clustering Coefficient'
  645. )
  646. # Share a single y-axis across all subplots, displayed on the far right only
  647. global_ymax = max(ax.get_ylim()[1] for ax in (ax1, ax2, ax3))
  648. for ax in (ax1, ax2, ax3):
  649. ax.set_ylim(0, global_ymax)
  650. ax.spines['top'].set_visible(False)
  651. for ax in (ax1, ax2):
  652. ax.spines['left'].set_visible(False)
  653. ax.spines['right'].set_visible(False)
  654. ax.tick_params(axis='y', which='both', left=False, right=False, labelleft=False)
  655. ax.tick_params(axis='x', which='both', bottom=False, labelbottom=False)
  656. ax3.spines['left'].set_visible(False)
  657. ax3.spines['right'].set_visible(True)
  658. ax3.yaxis.tick_right()
  659. ax3.yaxis.set_label_position('right')
  660. ax3.set_ylabel('Clustering Coefficient', fontsize=14, labelpad=8)
  661. plt.tight_layout()
  662. plt.savefig('../results/clustering_coefficient.svg', bbox_inches='tight', dpi=300)
  663. plt.savefig('../results/clustering_coefficient.png', bbox_inches='tight', dpi=600)
  664. plt.show()
  665. # %%
  666. def create_barplot(data1, data2, data3, title, ax, name):
  667. df = pd.DataFrame({
  668. 'Control': [data1],
  669. 'PCD': [data2],
  670. 'SCA': [data3]
  671. })
  672. df_melted = df.melt(var_name='Group', value_name=name)
  673. custom_palette = {'PCD': 'cornflowerblue', 'Control': 'turquoise', 'SCA': 'orange'}
  674. sns.barplot(
  675. x='Group', y=name, data=df_melted, palette=custom_palette, ax=ax,
  676. width=0.5, saturation=0.6,
  677. )
  678. ax.set_title(title, fontsize=14)
  679. ax.set_xlabel(None)
  680. ax.set_ylabel(None)
  681. ax.spines['top'].set_visible(False)
  682. ax.spines['right'].set_visible(False)
  683. fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(8, 4))
  684. plt.subplots_adjust(wspace=0.35)
  685. metric_to_plot = 'num_edges'
  686. create_barplot(
  687. metrics1[metric_to_plot],
  688. metrics2[metric_to_plot],
  689. metrics3[metric_to_plot],
  690. 'Number of edges', ax1, 'Number of edges'
  691. )
  692. metric_to_plot = 'global_clustering_coefficient'
  693. create_barplot(
  694. metrics1[metric_to_plot],
  695. metrics2[metric_to_plot],
  696. metrics3[metric_to_plot],
  697. 'Global clustering coefficient', ax2, 'Global clustering coefficient'
  698. )
  699. metric_to_plot = 'global_efficiency'
  700. create_barplot(
  701. metrics1[metric_to_plot],
  702. metrics2[metric_to_plot],
  703. metrics3[metric_to_plot],
  704. 'Global efficiency', ax3, 'Global efficiency'
  705. )
  706. # Show x-axis group labels only on the rightmost subplot; keep all y-axes (different scales)
  707. for ax in (ax1, ax2):
  708. ax.tick_params(axis='x', which='both', bottom=False, labelbottom=False)
  709. plt.tight_layout()
  710. plt.savefig('../results/edges_clustering_coefficient_global_efficiency.svg', bbox_inches='tight', dpi=300)
  711. plt.savefig('../results/edges_clustering_coefficient_global_efficiency.png', bbox_inches='tight', dpi=600)
  712. plt.show()
  713. # %% [markdown]
  714. # ### Other
  715. # %%
  716. # ──────────────────────────────────────────────────────────────
  717. # SENSITIVITY ANALYSIS: effect of correlation threshold on
  718. # key network metrics (Reviewer Major Point 3)
  719. # ──────────────────────────────────────────────────────────────
  720. sensitivity_thresholds = [0.3, 0.4, 0.5, 0.6, 0.7]
  721. sensitivity_rows = []
  722. for thr in sensitivity_thresholds:
  723. adj_g1 = np.where(np.abs(corr_matrix_group1) > thr, corr_matrix_group1, 0)
  724. adj_g2 = np.where(np.abs(corr_matrix_group2) > thr, corr_matrix_group2, 0)
  725. adj_g3 = np.where(np.abs(corr_matrix_group3) > thr, corr_matrix_group3, 0)
  726. np.fill_diagonal(adj_g1, 0)
  727. np.fill_diagonal(adj_g2, 0)
  728. np.fill_diagonal(adj_g3, 0)
  729. _g1 = nx.from_numpy_array(adj_g1)
  730. _g2 = nx.from_numpy_array(adj_g2)
  731. _g3 = nx.from_numpy_array(adj_g3)
  732. for label, _g in [('Control', _g1), ('PCD', _g2), ('SCA1', _g3)]:
  733. n = _g.number_of_nodes()
  734. sensitivity_rows.append({
  735. 'Threshold': thr,
  736. 'Group': label,
  737. 'Edge Count': _g.number_of_edges(),
  738. 'Density': nx.density(_g),
  739. 'Global Clustering': nx.average_clustering(_g),
  740. 'Global Efficiency': nx.global_efficiency(_g),
  741. 'Avg Node Strength': sum(dict(_g.degree(weight='weight')).values()) / n,
  742. 'Avg Betweenness Centrality': float(np.mean(list(nx.betweenness_centrality(_g).values()))),
  743. })
  744. sensitivity_df = pd.DataFrame(sensitivity_rows)
  745. print("=== Sensitivity Analysis: Network Metrics Across Thresholds ===\n")
  746. for thr in sensitivity_thresholds:
  747. sub = sensitivity_df[sensitivity_df['Threshold'] == thr]
  748. print(f"\n--- Threshold |r| > {thr} ---")
  749. print(sub.to_string(index=False))
  750. print()
  751. sensitivity_df.to_csv('../results/sensitivity_threshold_analysis.csv', index=False)
  752. print("\nSaved to ../results/sensitivity_threshold_analysis.csv")
  753. # %%
  754. # ──────────────────────────────────────────────────────────────
  755. # PROPORTIONAL THRESHOLDING (Reviewer Major Point 3 supplement)
  756. # Keep the top X% strongest edges, compare with absolute thresholds.
  757. # ──────────────────────────────────────────────────────────────
  758. density_targets = [0.10, 0.20, 0.30, 0.40, 0.50]
  759. prop_rows = []
  760. for group_label, corr_mat in [('Control', corr_matrix_group1),
  761. ('PCD', corr_matrix_group2),
  762. ('SCA1', corr_matrix_group3)]:
  763. n = corr_mat.shape[0]
  764. upper = np.abs(corr_mat[np.triu_indices(n, k=1)])
  765. for target_density in density_targets:
  766. n_edges_target = int(target_density * len(upper))
  767. if n_edges_target == 0:
  768. continue
  769. sorted_vals = np.sort(upper)[::-1]
  770. cutoff = sorted_vals[min(n_edges_target - 1, len(sorted_vals) - 1)]
  771. adj = np.where(np.abs(corr_mat) >= cutoff, corr_mat, 0)
  772. np.fill_diagonal(adj, 0)
  773. g = nx.from_numpy_array(adj)
  774. prop_rows.append({
  775. 'Group': group_label,
  776. 'Target Density': target_density,
  777. 'Actual Density': nx.density(g),
  778. 'Edge Count': g.number_of_edges(),
  779. 'Global Clustering': nx.average_clustering(g),
  780. 'Global Efficiency': nx.global_efficiency(g),
  781. 'Avg Node Strength': sum(dict(g.degree(weight='weight')).values()) / n,
  782. })
  783. prop_df = pd.DataFrame(prop_rows)
  784. print("=== Proportional Thresholding: Network Metrics ===\n")
  785. for grp in ['Control', 'PCD', 'SCA1']:
  786. sub = prop_df[prop_df['Group'] == grp]
  787. print(f"\n--- {grp} ---")
  788. print(sub.to_string(index=False))
  789. prop_df.to_csv('../results/proportional_threshold_analysis.csv', index=False)
  790. print("\nSaved to ../results/proportional_threshold_analysis.csv")
  791. # %%
  792. # threshold sensitivity
  793. import seaborn as sns
  794. sens = pd.read_csv('../results/sensitivity_threshold_analysis.csv')
  795. custom_palette = {'PCD': 'cornflowerblue',
  796. 'Control': 'turquoise',
  797. 'SCA1': 'orange'}
  798. non_metric_cols = {'Threshold', 'Group'}
  799. metrics = [(col, col) for col in sens.columns if col not in non_metric_cols]
  800. n_metrics = len(metrics)
  801. ncols = 6
  802. nrows = int(np.ceil(n_metrics / ncols))
  803. fig, axes = plt.subplots(nrows, ncols, figsize=(2.8 * ncols, 3.0 * nrows), sharex=True)
  804. axes_flat = axes.ravel() if nrows > 1 else axes
  805. for ax, (col, label) in zip(axes_flat, metrics):
  806. for group, color in custom_palette.items():
  807. sub = sens[sens['Group'] == group].sort_values('Threshold')
  808. ax.plot(sub['Threshold'], sub[col], marker='o', markersize=5,
  809. linewidth=1.6, color=color, label=group)
  810. ax.axvline(0.5, color='gray', linestyle='--', lw=1, alpha=0.7)
  811. ax.set_ylabel(label)
  812. ax.grid(True, axis='y', alpha=0.25)
  813. ax.spines['top'].set_visible(False)
  814. ax.spines['right'].set_visible(False)
  815. for ax in axes_flat[n_metrics:]:
  816. ax.set_visible(False)
  817. axes_flat[0].set_xlabel('|r| threshold')
  818. axes_flat[0].legend(loc='best', fontsize=9, frameon=False)
  819. plt.tight_layout()
  820. plt.savefig('../results/sensitivity_threshold.svg', bbox_inches='tight', dpi=300)
  821. plt.savefig('../results/sensitivity_threshold.png', bbox_inches='tight', dpi=600)
  822. plt.show()
  823. # %%
  824. def compute_network_metrics(g):
  825. node_count = len(g.nodes)
  826. edge_count = len(g.edges)
  827. avg_degree = sum(dict(g.degree()).values()) / node_count
  828. density = nx.density(g)
  829. clustering_coefficient = nx.average_clustering(g)
  830. if nx.is_connected(g):
  831. avg_path_length = nx.average_shortest_path_length(g)
  832. else:
  833. avg_path_length = float('inf') # If the graph is not connected
  834. assortativity = nx.degree_assortativity_coefficient(g)
  835. if nx.is_connected(g):
  836. diameter = nx.diameter(g)
  837. else:
  838. diameter = float('inf') # If the graph is not connected
  839. avg_node_strength = sum(dict(g.degree(weight='weight')).values()) / node_count
  840. return {
  841. 'Edge Count': edge_count,
  842. 'Average Degree': avg_degree,
  843. 'Density': density,
  844. 'Clustering Coefficient': clustering_coefficient,
  845. 'Average Path Length': avg_path_length,
  846. 'Assortativity': assortativity,
  847. 'Diameter': diameter,
  848. 'Average Node Strength': avg_node_strength
  849. }
  850. control_metrics = compute_network_metrics(g1)
  851. anti_yo_pcd_metrics = compute_network_metrics(g2)
  852. sca_metrics = compute_network_metrics(g3)
  853. # %%
  854. metrics_dict = {
  855. 'Control': control_metrics,
  856. 'Anti-yo PCD': anti_yo_pcd_metrics,
  857. 'SCA': sca_metrics
  858. }
  859. df = pd.DataFrame.from_dict(metrics_dict, orient='index')
  860. # Convert the DataFrame to LaTeX format
  861. latex_output = df.to_latex(index=True, float_format="%.2f")
  862. # Display or save the LaTeX output
  863. print(latex_output)
  864. with open("../results/network_results.tex", "w") as f:
  865. f.write(latex_output)

network_analysis.ipynb at commit 6b7297b, no license · at the source

Overview

Authors: Tamara Künzle1, Lucas Rincon de la Rosa1, Clément Vialatte de Pémille2,3, Alice Leprince-Laurenge1,4, Alberto Picca1,4, Giulia Coarelli1,4, Delphine Leclercq1,5, Alexandra Dürr6, Dimitri Psimaras1,4, Agusti Alentorn1,4
ORCID iDs: Agusti Alentorn
  1. ICM, INSERM U 1127, CNRS UMR 7225, UMRS 1127, Paris Brain Institute, Sorbonne University, Paris, France
  2. Neurological Department, Groupe Hospitalier Paris Saint-Joseph, Paris, France
  3. Université Paris Cité, Paris, France
  4. Charles Foix, DMU Neurosciences, Service de Neuro-Oncologie-Institut de Neurologie, AP-HP, Hôpitaux Universitaires La Pitié Salpêtrière, Paris, France
  5. Department of Neuroradiology, APHP, La Pitié-Salpêtrière Hospital, Sorbonne University, F-75013 Paris, France
  6. APHP-Salpêtrière Hospital, DMU BioGem, CNRS, INSERM, Paris Brain Institute, Sorbonne University, Paris, France
Journal: Journal of neurology, volume 273, issue 8, article 493
Dates: received 1 March 2026; accepted 13 May 2026; published online 27 July 2026; in print 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1007/s00415-026-13884-0 · PMID 42509533 · PMCID PMC13407450 · OpenAlex W7171430825
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), other condition (population), clinical / translational (subfield)
Methods: Connectivity, Spectral & time-frequency, Statistics, Machine learning, Preprocessing, Graphs
Keywords: Neurological paraneoplastic syndrome, Cerebellar ataxia, Cerebellar atrophy, Cortical thickness and connectivity
MeSH: Cerebellum*, Nerve Net*, Ovarian Neoplasms*, Paraneoplastic Cerebellar Degeneration*, Spinocerebellar Ataxias*, Adult, Aged, Ataxin-1, Atrophy, Autoantibodies, Female, Humans, Magnetic Resonance Imaging, Middle Aged (* major topic)
Topic: Autoimmune Neurological Disorders and Treatments (Neurology, Medicine), according to OpenAlex
Funding: Agence Nationale de la Recherche (2025-PEPR-121554, ANR-23-CE17-0027); Institut National Du Cancer (MultiPOLA-OSIRIS25)
Citations: not cited yet (Europe PMC); 44 references in the paper

Abstract

Background: Anti-Yo paraneoplastic cerebellar degeneration (PCD) is a rare autoimmune disorder linked to ovarian and breast cancers. Neurological symptoms often precede cancer diagnosis, yet conventional imaging techniques may fail to detect early cerebellar changes. This study quantitatively assessed cerebellar atrophy and network alterations in anti-Yo PCD patients compared to healthy controls and patients with spinocerebellar ataxia type 1 (SCA1).

Methods: We analyzed structural MRI data from 11 antiYo PCD patients, 17 healthy controls, and 17 SCA1 patients. Cerebellar lobular segmentation and cortical thickness measurements were conducted. Structural covariance networks were built using inter-lobular Pearson correlation coefficients (threshold |r| > 0.5), with graph theory metrics assessing connectivity. Univariate and age-adjusted multivariate analyses evaluated group differences, and machine learning assessed the discriminative power of regional morphometric measures.

Results: AntiYo PCD patients showed pronounced anterior cortical thinning, while SCA1 atrophy was milder and more posterior. Two PCD subtypes emerged: one with severe atrophy, another with nearnormal thickness. Network analysis revealed increased node strength and clustering coefficients, but reduced betweenness centrality in PCD, suggesting altered network hierarchy and widespread clustering that may reflect pathological reorganization. In cross-validated analysis, regional cerebellar features distinguished PCD, SCA1, and controls with promising AUC values.

Conclusions: Anti-Yo PCD is characterized by anterior cerebellar vulnerability and network reorganization distinct from SCA1. These morphometric and connectivity markers are candidate imaging biomarkers for early diagnosis and subgroup stratification in paraneoplastic cerebellar degeneration.

Supplementary Information: The online version contains supplementary material available at 10.1007/s00415-026-13884-0.

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 12 matches between paragraphs and lines of code.

tamelenak/pcd-spatial-analysis

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 6b7297ba30111482bd35ca183a4707d7f10175d4, 6 May 2026
Languages: Jupyter (5), Python (1)
Size: 14 files, 6 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, environment (environment.yml, requirements.txt), 5 notebooks
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: Matplotlib (5 files), NumPy (5 files), pandas (5 files), scikit-learn (5 files), SciPy (5 files), seaborn (5 files), NiBabel (3 files), Nilearn (3 files), statsmodels (3 files), Plotly (2 files), NetworkX (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;
  • 5 scripts, each with its path and the digest of its content;
  • 12 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

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

Data

Datasets cited

Data availability

The dataset is available under 10.5281/zenodo.16094263. All code used for data analysis and figure generation is available at: https://github.com/tamelenak/pcd-spatial-analysis.

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 2, 28 September 2026

  • Publisher: n/a → Springer Science+Business Media

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 10 authors, 4 keywords, 14 MeSH terms, 2 funders, 27 references.

Cite

This paper

Künzle, T., Rincon de la Rosa, L., Vialatte de Pémille, C., Leprince-Laurenge, A., Picca, A., Coarelli, G., Leclercq, D., Dürr, A., Psimaras, D., & Alentorn, A. (2026). Spatial analysis of paraneoplastic cerebellar degeneration in ovarian cancer with anti-Yo syndrome and SCA1. Journal of neurology, 273(8), 493. https://doi.org/10.1007/s00415-026-13884-0

BibTeX

@article{kunzle2026spatial,
author = {Künzle, Tamara and Rincon de la Rosa, Lucas and Vialatte de Pémille, Clément and Leprince-Laurenge, Alice and Picca, Alberto and Coarelli, Giulia and Leclercq, Delphine and Dürr, Alexandra and Psimaras, Dimitri and Alentorn, Agusti},
title = {{Spatial analysis of paraneoplastic cerebellar degeneration in ovarian cancer with anti-Yo syndrome and SCA1}},
journal = {Journal of neurology},
year = {2026},
month = jul,
volume = {273},
number = {8},
pages = {493},
publisher = {Springer Science+Business Media},
issn = {0340-5354},
doi = {10.1007/s00415-026-13884-0},
url = {https://doi.org/10.1007/s00415-026-13884-0},
pmid = {42509533},
pmcid = {PMC13407450}
}

RIS

TY - JOUR
AU - Künzle, Tamara
AU - Rincon de la Rosa, Lucas
AU - Vialatte de Pémille, Clément
AU - Leprince-Laurenge, Alice
AU - Picca, Alberto
AU - Coarelli, Giulia
AU - Leclercq, Delphine
AU - Dürr, Alexandra
AU - Psimaras, Dimitri
AU - Alentorn, Agusti
TI - Spatial analysis of paraneoplastic cerebellar degeneration in ovarian cancer with anti-Yo syndrome and SCA1
T2 - Journal of neurology
J2 - J Neurol
PY - 2026
DA - 2026/07/27
VL - 273
IS - 8
SP - 493
SN - 0340-5354
PB - Springer Science+Business Media
DO - 10.1007/s00415-026-13884-0
UR - https://doi.org/10.1007/s00415-026-13884-0
LA - en
ER -

CSL-JSON

{
"id": "10.1007/s00415-026-13884-0",
"type": "article-journal",
"title": "Spatial analysis of paraneoplastic cerebellar degeneration in ovarian cancer with anti-Yo syndrome and SCA1",
"container-title": "Journal of neurology",
"author": [
{
"family": "Künzle",
"given": "Tamara"
},
{
"family": "Rincon de la Rosa",
"given": "Lucas"
},
{
"family": "Vialatte de Pémille",
"given": "Clément"
},
{
"family": "Leprince-Laurenge",
"given": "Alice"
},
{
"family": "Picca",
"given": "Alberto"
},
{
"family": "Coarelli",
"given": "Giulia"
},
{
"family": "Leclercq",
"given": "Delphine"
},
{
"family": "Dürr",
"given": "Alexandra"
},
{
"family": "Psimaras",
"given": "Dimitri"
},
{
"family": "Alentorn",
"given": "Agusti"
}
],
"container-title-short": "J Neurol",
"volume": "273",
"issue": "8",
"page": "493",
"DOI": "10.1007/s00415-026-13884-0",
"PMID": "42509533",
"PMCID": "PMC13407450",
"ISSN": "0340-5354",
"publisher": "Springer Science+Business Media",
"URL": "https://doi.org/10.1007/s00415-026-13884-0",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
27
]
]
}
}

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/s41467-026-73996-z [code]
Genetic architecture of white matter microstructure captured by unsupervised deep representation learning of fractional anisotropy maps.
Journal: Nature communications
In common: Nilearn, Plotly, NiBabel, 7 other tools, structural MRI / diffusion, 1 reference
[2] doi:10.1073/pnas.2531706123 [code]
Metabolism-weighted brain connectome reveals synaptic integration and vulnerability to neurodegeneration.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: Nilearn, NetworkX, Plotly, 8 other tools, other condition
[3] doi:10.1038/s41467-026-71428-6 [code]
Binding items to contexts through conjunctive neural representations with the method of loci.
Journal: Nature communications
In common: Nilearn, Plotly, NiBabel, 7 other tools, 1 reference
[4] doi:10.1162/imag.a.1276 [code]
High-resolution whole-brain magnetic resonance spectroscopic imaging in youth at risk for psychosis.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, NetworkX, Plotly, 7 other tools, clinical / translational
[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: Nilearn, Plotly, NiBabel, 7 other tools, structural MRI / diffusion
[6] doi:10.1002/hbm.70469 [code]
VarCoNet: A Variability-Aware Self-Supervised Framework for Functional Connectome Extraction From Resting-State fMRI.
Journal: Human brain mapping
In common: Nilearn, NetworkX, Plotly, 7 other tools
[7] doi:10.1038/s41398-026-04157-5 [code]
Association of glymphatic function with 40-Hz neural oscillations, systemic metabolic markers, and cognitive performance in healthy aging adults: An EEG and MRI study.
Journal: Translational psychiatry
In common: Nilearn, NetworkX, NiBabel, 6 other tools, structural MRI / diffusion, clinical / translational
[8] doi:10.1212/wnl.0000000000218472 [code]
Lesion-Level Subtypes of White Matter Hyperintensity Evolution Beyond Spatial Location.
Journal: Neurology
In common: Nilearn, Plotly, NiBabel, 6 other tools, structural MRI / diffusion, clinical / translational
[9] doi:10.1038/s41467-026-72931-6 [code]
Three parsimonious spatiotemporal patterns in cerebellum reveal individual traits in function and behavior.
Journal: Nature communications
In common: Nilearn, NiBabel, statsmodels, 6 other tools, 1 reference
[10] doi:10.1038/s41467-026-75959-w [code]
Charting higher-order models of brain function beyond pairwise interactions.
Journal: Nature communications
In common: Nilearn, NetworkX, NiBabel, 7 other tools

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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