OSCR

Toward a transcriptomic framework for ultrasound neuromodulation: A perspective on gene expression and regional brain sensitivity.

Code ↔ Paper

13 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 13 matches
  1. [1] § Materials and Methods › Gene selection through systematic PubMed literature search ↔ scripts/create_gene_selection_flowchart.py, lines 60–209 · score 0.93 · SCN9A, expert review, PubMed, Allen Human, PLD3, gene families
  2. [2] § Materials and Methods › Gene selection through systematic PubMed literature search ↔ scripts/systematic_gene_selection.py, lines 1–37 · score 0.92 · HGNC nomenclature, broader mechanotransduction, inclusion threshold, Allen Human Brain, PubMed, enumerated
  3. [3] § Spatial Transcriptomic Analysis of Ultrasound-Relevant Genes ↔ tusgene/config.py, lines 149–222 · score 0.88 · voltage gated sodium, potassium channels, chloride channels, direct mechanosensitivity, mechanosensitive channel, inwardly
  4. [4] § Materials and Methods › Dimensionality reduction and clustering ↔ tusgene/visualization.py, lines 911–1004 · score 0.88 · trivial cortex, subcortex split, confidence intervals, random seeds, robust silhouette, silhouette scores
  5. [5] § Spatial Transcriptomic Analysis of Ultrasound-Relevant Genes ↔ tusgene/visualization.py, lines 2189–2232 · score 0.88 · Glass brain projections, Bidirectional radar, Axial slices, axial view, brain renderings, gene family expression
  6. [6] § Spatial Transcriptomic Analysis of Ultrasound-Relevant Genes ↔ scripts/create_mainFig_fast.py, lines 1–25 · score 0.87 · Glass brain projections, Bidirectional radar, Axial slices, axial view, brain renderings, cluster highlighted
  7. [7] § Spatial Transcriptomic Analysis of Ultrasound-Relevant Genes ↔ pubMed/systematic_gene_identification.py, lines 1–43 · score 0.86 · voltage gated channels, systematic literature review, gap junctions, mechanosensitive ion channels, mechanosensitive gene expression, lipid
  8. [8] § Materials and Methods › Dimensionality reduction and clustering ↔ tusgene/statistics.py, lines 198–325 · score 0.82 · Monte Carlo validation, Adjusted Rand, random seeds, silhouette scores, architecture, dual
  9. [9] § Spatial Transcriptomic Analysis of Ultrasound-Relevant Genes ↔ pubMed/extract_genes.py, lines 8–83 · score 0.81 · voltage gated sodium, inwardly rectifying, potassium channels, gene families, Piezo1, caveolin
  10. [10] § Spatial Transcriptomic Analysis of Ultrasound-Relevant Genes ↔ tusgene/statistics.py, lines 198–325 · score 0.80 · n_init, Monte Carlo validation, random seeds, silhouette scores, random gene, moderate
  11. [11] § Diverse Genetic Mechanisms Underlying Ultrasound Neuromodulation ↔ pubMed/extract_genes.py, lines 8–83 · score 0.78 · lipid mediated signaling, mechanosensitive ion channels, GM1, kinase, TRPA1, transduction
  12. [12] § Materials and Methods › Monte Carlo simulation ↔ tusgene/statistics.py, lines 120–195 · score 0.71 · Adjusted Rand, Monte Carlo, Silhouette scores, TUS gene cluster, random gene, iteration
  13. [13] § Materials and Methods › Preprocessing ↔ tusgene/data.py, lines 14–53 · score 0.66 · Allen Human Brain, subcortical regions, Gene expression, Atlas

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 · 731 lines · 26 KB · no license · 3 matches

  1. """
  2. Statistical Analysis Functions
  3. ==============================
  4. Monte Carlo validation and gene-level statistics for TUS clustering analysis.
  5. Key Analyses:
  6. 1. Monte Carlo Validation: Tests whether TUS genes cluster differently than
  7. random gene sets, addressing whether observed clustering is TUS-specific
  8. or driven by general brain architecture.
  9. 2. Gene-Level Statistics: Kruskal-Wallis tests and Cohen's d effect sizes
  10. to identify which genes drive cluster differences.
  11. """
  12. import numpy as np
  13. import pandas as pd
  14. from scipy import stats
  15. from sklearn.decomposition import PCA
  16. from sklearn.cluster import KMeans
  17. from sklearn.metrics import silhouette_score, adjusted_rand_score
  18. from tqdm import tqdm
  19. from . import config
  20. # =============================================================================
  21. # ROBUST SILHOUETTE ANALYSIS
  22. # =============================================================================
  23. def compute_robust_silhouette_curve(expression_matrix, k_range=None, n_seeds=100, n_init=10):
  24. """
  25. Compute silhouette scores with proper statistical methodology.
  26. Standard K-means is sensitive to random initialization. This function
  27. addresses this by:
  28. 1. Using n_init=10 (run K-means 10 times, keep best)
  29. 2. Averaging across multiple random seeds
  30. 3. Computing confidence intervals
  31. This provides publication-quality, reproducible results.
  32. Args:
  33. expression_matrix: Expression matrix (n_regions x n_genes)
  34. k_range: Range of K values to test. Default: config.K_RANGE
  35. n_seeds: Number of random seeds to average over (default: 100)
  36. n_init: Number of K-means initializations per run (default: 10)
  37. Returns:
  38. results_df: DataFrame with columns:
  39. - k: Number of clusters
  40. - mean_silhouette: Mean silhouette across seeds
  41. - std_silhouette: Standard deviation
  42. - ci95: 95% confidence interval half-width
  43. - is_local_max: Boolean indicating local maximum
  44. """
  45. if k_range is None:
  46. k_range = config.K_RANGE
  47. # PCA dimensionality reduction
  48. pca = PCA(n_components=config.N_PCA_COMPONENTS)
  49. expression_pca = pca.fit_transform(expression_matrix)
  50. print(f"Robust Silhouette Analysis:")
  51. print(f" K range: {min(k_range)}-{max(k_range)}")
  52. print(f" Random seeds: {n_seeds}")
  53. print(f" K-means initializations per seed: {n_init}")
  54. results = []
  55. for k in k_range:
  56. scores = []
  57. for seed in range(n_seeds):
  58. kmeans = KMeans(n_clusters=k, random_state=seed, n_init=n_init, init='k-means++')
  59. labels = kmeans.fit_predict(expression_pca)
  60. scores.append(silhouette_score(expression_pca, labels))
  61. mean_sil = np.mean(scores)
  62. std_sil = np.std(scores)
  63. # 95% CI using standard error of mean: z * (σ / √n)
  64. # z=1.96 is the two-tailed critical value from standard normal distribution
  65. # (captures 95% of probability mass, 2.5% in each tail)
  66. ci95 = 1.96 * std_sil / np.sqrt(n_seeds)
  67. results.append({
  68. 'k': k,
  69. 'mean_silhouette': mean_sil,
  70. 'std_silhouette': std_sil,
  71. 'ci95': ci95
  72. })
  73. print(f" K={k:2d}: {mean_sil:.4f} ± {ci95:.4f}")
  74. results_df = pd.DataFrame(results)
  75. # Identify local maxima
  76. sil_values = results_df['mean_silhouette'].values
  77. is_local_max = np.zeros(len(sil_values), dtype=bool)
  78. for i in range(1, len(sil_values) - 1):
  79. if sil_values[i] > sil_values[i-1] and sil_values[i] > sil_values[i+1]:
  80. is_local_max[i] = True
  81. results_df['is_local_max'] = is_local_max
  82. # Print local maxima
  83. local_max_df = results_df[results_df['is_local_max']]
  84. if not local_max_df.empty:
  85. print(f"\nLocal maxima at K = {local_max_df['k'].tolist()}")
  86. # Best K excluding K=2
  87. best_k3_plus = results_df[results_df['k'] >= 3].loc[
  88. results_df[results_df['k'] >= 3]['mean_silhouette'].idxmax()
  89. ]
  90. print(f"Best K (for K≥3): {int(best_k3_plus['k'])} (silhouette = {best_k3_plus['mean_silhouette']:.4f})")
  91. return results_df
  92. # =============================================================================
  93. # MONTE CARLO VALIDATION
  94. # =============================================================================
  95. def monte_carlo_clustering_validation(
  96. expression_df,
  97. tus_labels,
  98. n_iterations=None,
  99. n_genes=None,
  100. random_seed=None,
  101. show_progress=True
  102. ):
  103. """
  104. Monte Carlo validation of TUS gene clustering specificity.
  105. NOTE: This is the legacy single-K version. Use monte_carlo_dual_k_validation
  106. for comparing both K_OPTIMAL and K_GRANULAR simultaneously.
  107. """
  108. if n_iterations is None:
  109. n_iterations = config.N_MONTE_CARLO_ITERATIONS
  110. if n_genes is None:
  111. n_genes = len(config.TUS_GENES)
  112. if random_seed is None:
  113. random_seed = config.MONTE_CARLO_SEED
  114. all_genes = [col for col in expression_df.columns
  115. if col != 'label' and col not in config.TUS_GENES]
  116. print(f"Monte Carlo Validation:")
  117. print(f" Iterations: {n_iterations:,}")
  118. print(f" Genes per set: {n_genes}")
  119. print(f" Available non-TUS genes: {len(all_genes):,}")
  120. np.random.seed(random_seed)
  121. results = {
  122. 'silhouette': [],
  123. 'optimal_k': [],
  124. 'ari': []
  125. }
  126. iterator = range(n_iterations)
  127. if show_progress:
  128. iterator = tqdm(iterator, desc="Monte Carlo")
  129. for _ in iterator:
  130. random_genes = np.random.choice(all_genes, size=n_genes, replace=False)
  131. expression_random = expression_df[random_genes].to_numpy()
  132. pca = PCA(n_components=config.N_PCA_COMPONENTS)
  133. expression_pca = pca.fit_transform(expression_random)
  134. best_score = -1
  135. best_k = config.K_OPTIMAL
  136. best_labels = None
  137. for k in config.K_RANGE:
  138. kmeans = KMeans(n_clusters=k, random_state=0, n_init='auto')
  139. labels = kmeans.fit_predict(expression_pca)
  140. score = silhouette_score(expression_pca, labels)
  141. if score > best_score:
  142. best_score = score
  143. best_k = k
  144. best_labels = labels
  145. ari = adjusted_rand_score(tus_labels, best_labels)
  146. results['silhouette'].append(best_score)
  147. results['optimal_k'].append(best_k)
  148. results['ari'].append(ari)
  149. results_df = pd.DataFrame(results)
  150. print(f"\nResults Summary:")
  151. print(f" Mean Silhouette: {results_df['silhouette'].mean():.4f} +/- {results_df['silhouette'].std():.4f}")
  152. print(f" Mean Optimal K: {results_df['optimal_k'].mean():.1f}")
  153. print(f" Mean ARI: {results_df['ari'].mean():.4f} +/- {results_df['ari'].std():.4f}")
  154. return results_df
  155. def monte_carlo_dual_k_validation(
  156. expression_df,
  157. tus_labels_k_opt,
  158. tus_labels_k_granular,
  159. n_iterations=1000,
  160. n_genes=None,
  161. random_seed=None,
  162. show_progress=True
  163. ):
  164. """
  165. Monte Carlo validation comparing random gene sets to BOTH K_OPTIMAL and K_GRANULAR TUS clusters.
  166. SCIENTIFIC QUESTION:
  167. Do random gene sets produce the same brain parcellations as TUS genes?
  168. This tests whether TUS clustering reflects general brain architecture
  169. or is specific to mechanosensitive gene expression.
  170. METHOD:
  171. For each of N random gene sets:
  172. 1. Perform PCA + K-means at FIXED K values (matching TUS configuration)
  173. 2. Compute ARI between random clusters and TUS clusters at each K
  174. 3. Track silhouette scores for clustering quality
  175. INTERPRETATION OF ARI:
  176. - ARI > 0.7: Strong similarity (brain architecture dominates)
  177. - ARI 0.4-0.7: Moderate similarity (partial architecture effect)
  178. - ARI < 0.4: Weak similarity (TUS genes are specific)
  179. - ARI ~ 0: No similarity beyond chance
  180. Args:
  181. expression_df: Full expression DataFrame
  182. tus_labels_k_opt: TUS cluster labels at K_OPTIMAL
  183. tus_labels_k_granular: TUS cluster labels at K_GRANULAR
  184. n_iterations: Number of Monte Carlo iterations (default: 1000)
  185. n_genes: Number of genes per random set (default: 30)
  186. random_seed: Random seed for reproducibility
  187. show_progress: Show progress bar
  188. Returns:
  189. results_df: DataFrame with columns:
  190. - silhouette_k{K_OPTIMAL}, silhouette_k{K_GRANULAR}: Clustering quality
  191. - ari_k{K_OPTIMAL}, ari_k{K_GRANULAR}: Similarity to TUS clusters
  192. """
  193. if n_genes is None:
  194. n_genes = len(config.TUS_GENES)
  195. if random_seed is None:
  196. random_seed = config.MONTE_CARLO_SEED
  197. k_opt = config.K_OPTIMAL
  198. k_gran = config.K_GRANULAR
  199. # Get all available genes (excluding TUS genes)
  200. all_genes = [col for col in expression_df.columns
  201. if col != 'label' and col not in config.TUS_GENES]
  202. print(f"Monte Carlo Dual-K Validation:")
  203. print(f" Iterations: {n_iterations:,}")
  204. print(f" Genes per set: {n_genes}")
  205. print(f" Available non-TUS genes: {len(all_genes):,}")
  206. print(f" Comparing at K={k_opt} (optimal) and K={k_gran} (granular)")
  207. np.random.seed(random_seed)
  208. results = {
  209. f'silhouette_k{k_opt}': [],
  210. f'silhouette_k{k_gran}': [],
  211. f'ari_k{k_opt}': [],
  212. f'ari_k{k_gran}': []
  213. }
  214. iterator = range(n_iterations)
  215. if show_progress:
  216. iterator = tqdm(iterator, desc=f"Monte Carlo (K={k_opt} & K={k_gran})")
  217. for _ in iterator:
  218. # Sample random genes
  219. random_genes = np.random.choice(all_genes, size=n_genes, replace=False)
  220. expression_random = expression_df[random_genes].to_numpy()
  221. # PCA
  222. pca = PCA(n_components=config.N_PCA_COMPONENTS)
  223. expression_pca = pca.fit_transform(expression_random)
  224. # K-means at K_OPTIMAL
  225. kmeans_opt = KMeans(n_clusters=k_opt, random_state=0, n_init='auto')
  226. labels_opt = kmeans_opt.fit_predict(expression_pca)
  227. sil_opt = silhouette_score(expression_pca, labels_opt)
  228. ari_opt = adjusted_rand_score(tus_labels_k_opt, labels_opt)
  229. # K-means at K_GRANULAR
  230. kmeans_gran = KMeans(n_clusters=k_gran, random_state=0, n_init='auto')
  231. labels_gran = kmeans_gran.fit_predict(expression_pca)
  232. sil_gran = silhouette_score(expression_pca, labels_gran)
  233. ari_gran = adjusted_rand_score(tus_labels_k_granular, labels_gran)
  234. results[f'silhouette_k{k_opt}'].append(sil_opt)
  235. results[f'silhouette_k{k_gran}'].append(sil_gran)
  236. results[f'ari_k{k_opt}'].append(ari_opt)
  237. results[f'ari_k{k_gran}'].append(ari_gran)
  238. results_df = pd.DataFrame(results)
  239. # Print summary
  240. ari_opt_col = f'ari_k{k_opt}'
  241. ari_gran_col = f'ari_k{k_gran}'
  242. sil_opt_col = f'silhouette_k{k_opt}'
  243. sil_gran_col = f'silhouette_k{k_gran}'
  244. print(f"\nResults Summary:")
  245. print(f" K={k_opt}: Mean ARI = {results_df[ari_opt_col].mean():.4f} +/- {results_df[ari_opt_col].std():.4f}")
  246. print(f" Mean Silhouette = {results_df[sil_opt_col].mean():.4f}")
  247. print(f" K={k_gran}: Mean ARI = {results_df[ari_gran_col].mean():.4f} +/- {results_df[ari_gran_col].std():.4f}")
  248. print(f" Mean Silhouette = {results_df[sil_gran_col].mean():.4f}")
  249. # Interpretation
  250. ari_opt_mean = results_df[ari_opt_col].mean()
  251. ari_gran_mean = results_df[ari_gran_col].mean()
  252. print(f"\nInterpretation:")
  253. for k, ari in [(f'K={k_opt}', ari_opt_mean), (f'K={k_gran}', ari_gran_mean)]:
  254. if ari > 0.7:
  255. print(f" {k}: HIGH similarity - clustering largely reflects brain architecture")
  256. elif ari > 0.4:
  257. print(f" {k}: MODERATE similarity - partial brain architecture effect")
  258. else:
  259. print(f" {k}: LOW similarity - TUS genes show specific clustering")
  260. return results_df
  261. def compute_tus_vs_random_statistics(tus_silhouette, monte_carlo_results):
  262. """
  263. Compare TUS clustering quality to random gene sets.
  264. Args:
  265. tus_silhouette: Silhouette score from TUS gene clustering
  266. monte_carlo_results: DataFrame from monte_carlo_clustering_validation()
  267. Returns:
  268. stats_dict: Dictionary with comparison statistics
  269. """
  270. random_silhouettes = monte_carlo_results['silhouette'].values
  271. random_ari = monte_carlo_results['ari'].values
  272. # Silhouette percentile
  273. pct_lower = (random_silhouettes < tus_silhouette).mean() * 100
  274. # ARI interpretation
  275. pct_high_ari = (random_ari > 0.7).mean() * 100
  276. pct_moderate_ari = ((random_ari > 0.4) & (random_ari <= 0.7)).mean() * 100
  277. pct_low_ari = (random_ari <= 0.4).mean() * 100
  278. stats_dict = {
  279. 'tus_silhouette': tus_silhouette,
  280. 'random_silhouette_mean': random_silhouettes.mean(),
  281. 'random_silhouette_std': random_silhouettes.std(),
  282. 'silhouette_percentile': pct_lower,
  283. 'ari_mean': random_ari.mean(),
  284. 'ari_std': random_ari.std(),
  285. 'pct_high_ari': pct_high_ari,
  286. 'pct_moderate_ari': pct_moderate_ari,
  287. 'pct_low_ari': pct_low_ari
  288. }
  289. print(f"\nTUS vs Random Comparison:")
  290. print(f" TUS silhouette: {tus_silhouette:.4f}")
  291. print(f" Random silhouette: {random_silhouettes.mean():.4f} +/- {random_silhouettes.std():.4f}")
  292. print(f" TUS percentile: {pct_lower:.1f}% (lower = TUS clusters less compact)")
  293. print(f"\n ARI Distribution:")
  294. print(f" High similarity (>0.7): {pct_high_ari:.1f}%")
  295. print(f" Moderate (0.4-0.7): {pct_moderate_ari:.1f}%")
  296. print(f" Low similarity (<=0.4): {pct_low_ari:.1f}%")
  297. return stats_dict
  298. def monte_carlo_null_distribution(
  299. expression_df,
  300. tus_ari_opt,
  301. tus_ari_gran,
  302. n_reference_sets=100,
  303. n_comparisons_per_ref=100,
  304. n_genes=None,
  305. random_seed=None,
  306. show_progress=True
  307. ):
  308. """
  309. Build null distribution of ARI values: random gene set vs. random gene set.
  310. This answers: "How similar are ANY two random gene sets to each other?"
  311. By comparing TUS-vs-random ARI to this null, we can determine if TUS
  312. clustering is more or less similar to random than expected by chance.
  313. SCIENTIFIC QUESTION:
  314. Is TUS behaving like a typical gene set, or does it show unusual
  315. similarity/dissimilarity to random gene clusterings?
  316. METHOD:
  317. 1. Pick a random gene "reference" set, cluster brain regions
  318. 2. Compare to N other random gene sets via ARI
  319. 3. Repeat with M reference sets to build stable null distribution
  320. 4. Compare TUS-vs-random ARI to this null
  321. INTERPRETATION:
  322. - If TUS ARI is within null distribution: TUS behaves like any gene set
  323. - If TUS ARI > null (higher similarity): TUS converges on same architecture
  324. - If TUS ARI < null (lower similarity): TUS has specificity!
  325. Args:
  326. expression_df: Full expression DataFrame with all genes
  327. tus_ari_opt: Mean ARI from TUS vs random at K_OPTIMAL (from prior analysis)
  328. tus_ari_gran: Mean ARI from TUS vs random at K_GRANULAR (from prior analysis)
  329. n_reference_sets: Number of reference gene sets to use (default: 100)
  330. n_comparisons_per_ref: Comparisons per reference set (default: 100)
  331. n_genes: Genes per set (default: matches TUS count)
  332. random_seed: Random seed for reproducibility
  333. show_progress: Show progress bar
  334. Returns:
  335. results_dict: Dictionary containing:
  336. - null_ari_opt: Array of null ARI values at K_OPTIMAL
  337. - null_ari_gran: Array of null ARI values at K_GRANULAR
  338. - tus_percentile_opt: Where TUS ARI falls in null distribution
  339. - tus_percentile_gran: Where TUS ARI falls in null distribution
  340. - interpretation: Text interpretation of results
  341. """
  342. if n_genes is None:
  343. n_genes = len(config.TUS_GENES)
  344. if random_seed is None:
  345. random_seed = config.MONTE_CARLO_SEED
  346. # Get all available genes (excluding TUS genes for fair comparison)
  347. all_genes = [col for col in expression_df.columns
  348. if col != 'label' and col not in config.TUS_GENES]
  349. print(f"Null Distribution Analysis (Random vs Random):")
  350. print(f" Reference sets: {n_reference_sets}")
  351. print(f" Comparisons per reference: {n_comparisons_per_ref}")
  352. print(f" Total comparisons: {n_reference_sets * n_comparisons_per_ref:,}")
  353. print(f" Genes per set: {n_genes}")
  354. print(f" Available genes: {len(all_genes):,}")
  355. np.random.seed(random_seed)
  356. null_ari_opt = []
  357. null_ari_gran = []
  358. k_opt = config.K_OPTIMAL
  359. k_gran = config.K_GRANULAR
  360. iterator = range(n_reference_sets)
  361. if show_progress:
  362. iterator = tqdm(iterator, desc="Building null distribution")
  363. for ref_idx in iterator:
  364. # Select reference gene set
  365. ref_genes = np.random.choice(all_genes, size=n_genes, replace=False)
  366. ref_expression = expression_df[ref_genes].to_numpy()
  367. # Cluster reference set
  368. pca_ref = PCA(n_components=config.N_PCA_COMPONENTS)
  369. ref_pca = pca_ref.fit_transform(ref_expression)
  370. kmeans_ref_opt = KMeans(n_clusters=k_opt, random_state=0, n_init='auto')
  371. ref_labels_opt = kmeans_ref_opt.fit_predict(ref_pca)
  372. kmeans_ref_gran = KMeans(n_clusters=k_gran, random_state=0, n_init='auto')
  373. ref_labels_gran = kmeans_ref_gran.fit_predict(ref_pca)
  374. # Compare to other random gene sets
  375. # Get remaining genes (exclude reference genes)
  376. remaining_genes = [g for g in all_genes if g not in ref_genes]
  377. for comp_idx in range(n_comparisons_per_ref):
  378. # Select comparison gene set from remaining genes
  379. # If not enough remaining, sample with replacement from all genes
  380. if len(remaining_genes) >= n_genes:
  381. comp_genes = np.random.choice(remaining_genes, size=n_genes, replace=False)
  382. else:
  383. comp_genes = np.random.choice(all_genes, size=n_genes, replace=False)
  384. comp_expression = expression_df[comp_genes].to_numpy()
  385. # Cluster comparison set
  386. pca_comp = PCA(n_components=config.N_PCA_COMPONENTS)
  387. comp_pca = pca_comp.fit_transform(comp_expression)
  388. kmeans_comp_opt = KMeans(n_clusters=k_opt, random_state=0, n_init='auto')
  389. comp_labels_opt = kmeans_comp_opt.fit_predict(comp_pca)
  390. kmeans_comp_gran = KMeans(n_clusters=k_gran, random_state=0, n_init='auto')
  391. comp_labels_gran = kmeans_comp_gran.fit_predict(comp_pca)
  392. # Compute ARI between reference and comparison
  393. ari_opt = adjusted_rand_score(ref_labels_opt, comp_labels_opt)
  394. ari_gran = adjusted_rand_score(ref_labels_gran, comp_labels_gran)
  395. null_ari_opt.append(ari_opt)
  396. null_ari_gran.append(ari_gran)
  397. null_ari_opt = np.array(null_ari_opt)
  398. null_ari_gran = np.array(null_ari_gran)
  399. # Calculate where TUS falls in null distribution
  400. tus_percentile_opt = (null_ari_opt < tus_ari_opt).mean() * 100
  401. tus_percentile_gran = (null_ari_gran < tus_ari_gran).mean() * 100
  402. # Print results
  403. print(f"\nNull Distribution Results:")
  404. print(f" K={k_opt} (random vs random):")
  405. print(f" Mean ARI: {null_ari_opt.mean():.4f} ± {null_ari_opt.std():.4f}")
  406. print(f" TUS vs random ARI: {tus_ari_opt:.4f}")
  407. print(f" TUS percentile: {tus_percentile_opt:.1f}%")
  408. print(f" K={k_gran} (random vs random):")
  409. print(f" Mean ARI: {null_ari_gran.mean():.4f} ± {null_ari_gran.std():.4f}")
  410. print(f" TUS vs random ARI: {tus_ari_gran:.4f}")
  411. print(f" TUS percentile: {tus_percentile_gran:.1f}%")
  412. # Interpretation
  413. print(f"\nInterpretation:")
  414. for k, tus_ari, null_ari, pct in [
  415. (k_opt, tus_ari_opt, null_ari_opt, tus_percentile_opt),
  416. (k_gran, tus_ari_gran, null_ari_gran, tus_percentile_gran)
  417. ]:
  418. if pct > 95:
  419. interp = "TUS MORE similar to random than random-to-random (architecture-driven)"
  420. elif pct < 5:
  421. interp = "TUS LESS similar to random than random-to-random (TUS-SPECIFIC!)"
  422. else:
  423. interp = "TUS similarity to random is TYPICAL (no special specificity)"
  424. print(f" K={k}: {interp}")
  425. # Return with dynamic keys based on actual K values
  426. results_dict = {
  427. f'null_ari_k{k_opt}': null_ari_opt,
  428. f'null_ari_k{k_gran}': null_ari_gran,
  429. f'null_mean_k{k_opt}': null_ari_opt.mean(),
  430. f'null_std_k{k_opt}': null_ari_opt.std(),
  431. f'null_mean_k{k_gran}': null_ari_gran.mean(),
  432. f'null_std_k{k_gran}': null_ari_gran.std(),
  433. f'tus_ari_k{k_opt}': tus_ari_opt,
  434. f'tus_ari_k{k_gran}': tus_ari_gran,
  435. f'tus_percentile_k{k_opt}': tus_percentile_opt,
  436. f'tus_percentile_k{k_gran}': tus_percentile_gran
  437. }
  438. return results_dict
  439. # =============================================================================
  440. # GENE-LEVEL STATISTICS
  441. # =============================================================================
  442. def compute_gene_cluster_statistics(expression_zscore, labels, genes=None):
  443. """
  444. Compute statistics for each gene across clusters.
  445. Tests which genes show significant differential expression between clusters
  446. using Kruskal-Wallis H-test (non-parametric ANOVA) and Cohen's d effect sizes.
  447. Args:
  448. expression_zscore: Z-scored expression matrix (n_regions x n_genes)
  449. labels: Cluster assignments
  450. genes: Gene names. Default: config.TUS_GENES
  451. Returns:
  452. stats_df: DataFrame with columns:
  453. - gene: Gene name
  454. - category: Gene family
  455. - kruskal_H: Kruskal-Wallis H statistic
  456. - kruskal_p: p-value
  457. - cluster{i}_mean_z: Mean z-score for cluster i
  458. - cohens_d_C{i}_vs_C{j}: Effect size between clusters
  459. """
  460. if genes is None:
  461. genes = config.TUS_GENES
  462. k = len(np.unique(labels))
  463. results = []
  464. for gene_idx, gene in enumerate(genes):
  465. expression = expression_zscore[:, gene_idx]
  466. # Group expression by cluster
  467. groups = [expression[labels == c] for c in range(k)]
  468. # Kruskal-Wallis test (non-parametric one-way ANOVA)
  469. H, p = stats.kruskal(*groups)
  470. row = {
  471. 'gene': gene,
  472. 'category': config.GENE_CATEGORIES.get(gene, 'Unknown'),
  473. 'kruskal_H': H,
  474. 'kruskal_p': p
  475. }
  476. # Cluster means
  477. for c in range(k):
  478. row[f'cluster{c+1}_mean_z'] = groups[c].mean()
  479. # Cohen's d effect sizes between all pairs
  480. for i in range(k):
  481. for j in range(i+1, k):
  482. d = cohens_d(groups[i], groups[j])
  483. row[f'cohens_d_C{i+1}_vs_C{j+1}'] = d
  484. results.append(row)
  485. stats_df = pd.DataFrame(results)
  486. # Sort by significance
  487. stats_df = stats_df.sort_values('kruskal_p')
  488. # Print top genes
  489. print(f"\nTop 10 genes by cluster differentiation (Kruskal-Wallis):")
  490. for _, row in stats_df.head(10).iterrows():
  491. print(f" {row['gene']:8s} ({row['category']:8s}): H={row['kruskal_H']:6.1f}, p={row['kruskal_p']:.2e}")
  492. return stats_df
  493. def cohens_d(group1, group2):
  494. """
  495. Compute Cohen's d effect size between two groups.
  496. Cohen's d = (mean1 - mean2) / pooled_std
  497. Interpretation:
  498. |d| < 0.2: Small effect
  499. |d| 0.2-0.8: Medium effect
  500. |d| > 0.8: Large effect
  501. Args:
  502. group1, group2: Arrays of values
  503. Returns:
  504. d: Cohen's d effect size
  505. """
  506. n1, n2 = len(group1), len(group2)
  507. # Use ddof=1 for sample variance (required for Cohen's d formula)
  508. var1, var2 = group1.var(ddof=1), group2.var(ddof=1)
  509. # Pooled standard deviation
  510. pooled_std = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))
  511. if pooled_std == 0:
  512. return 0.0
  513. d = (group1.mean() - group2.mean()) / pooled_std
  514. return d
  515. def identify_cluster_marker_genes(stats_df, threshold_p=0.05, threshold_d=0.8):
  516. """
  517. Identify genes that are markers for specific clusters.
  518. A marker gene shows:
  519. 1. Significant differential expression (p < threshold)
  520. 2. Large effect size (|d| > threshold) for at least one cluster pair
  521. Args:
  522. stats_df: Output from compute_gene_cluster_statistics()
  523. threshold_p: Significance threshold
  524. threshold_d: Minimum effect size threshold
  525. Returns:
  526. markers_df: DataFrame of marker genes with their characterization
  527. """
  528. # Filter significant genes
  529. sig_genes = stats_df[stats_df['kruskal_p'] < threshold_p].copy()
  530. # Find effect size columns
  531. d_cols = [c for c in stats_df.columns if c.startswith('cohens_d')]
  532. # Identify large effects
  533. markers = []
  534. for _, row in sig_genes.iterrows():
  535. large_effects = []
  536. for col in d_cols:
  537. if abs(row[col]) > threshold_d:
  538. clusters = col.replace('cohens_d_', '').split('_vs_')
  539. direction = 'higher in ' + clusters[0] if row[col] > 0 else 'higher in ' + clusters[1]
  540. large_effects.append({
  541. 'comparison': col.replace('cohens_d_', ''),
  542. 'd': row[col],
  543. 'direction': direction
  544. })
  545. if large_effects:
  546. markers.append({
  547. 'gene': row['gene'],
  548. 'category': row['category'],
  549. 'p_value': row['kruskal_p'],
  550. 'max_effect_size': max(abs(row[col]) for col in d_cols),
  551. 'n_large_effects': len(large_effects),
  552. 'effects': large_effects
  553. })
  554. markers_df = pd.DataFrame(markers)
  555. if not markers_df.empty:
  556. markers_df = markers_df.sort_values('max_effect_size', ascending=False)
  557. print(f"\nCluster Marker Genes (p<{threshold_p}, |d|>{threshold_d}):")
  558. print(f" Found {len(markers_df)} marker genes")
  559. for _, row in markers_df.head(5).iterrows():
  560. print(f" {row['gene']}: max |d| = {row['max_effect_size']:.2f}")
  561. return markers_df
  562. def compute_bonferroni_correction(stats_df, alpha=0.05):
  563. """
  564. Apply Bonferroni correction for multiple testing.
  565. Args:
  566. stats_df: DataFrame with 'kruskal_p' column
  567. alpha: Family-wise error rate
  568. Returns:
  569. stats_df with additional columns:
  570. - bonferroni_p: Corrected p-value
  571. - significant_bonferroni: Boolean
  572. """
  573. n_tests = len(stats_df)
  574. stats_df = stats_df.copy()
  575. stats_df['bonferroni_p'] = stats_df['kruskal_p'] * n_tests
  576. stats_df['bonferroni_p'] = stats_df['bonferroni_p'].clip(upper=1.0)
  577. stats_df['significant_bonferroni'] = stats_df['bonferroni_p'] < alpha
  578. n_sig = stats_df['significant_bonferroni'].sum()
  579. print(f"\nBonferroni correction (alpha={alpha}, {n_tests} tests):")
  580. print(f" {n_sig} genes remain significant")
  581. return stats_df

statistics.py at commit 9ef7e4d, no license · at the source

Overview

Authors: Joline M. Fan1,2,3, Ehsan Tadayon1,2, Andrew D. Krystal1,2,3, Keith R. Murphy4
ORCID iDs: Joline M. Fan
  1. Department of Neurology, University of California, San Francisco, CA, United States
  2. Weill Institute for Neurosciences, University of California, San Francisco, CA, United States
  3. Department of Psychiatry and Behavioral Sciences, University of California, San Francisco, CA, United States
  4. Attune Neurosciences, San Francisco, CA, United States
Institutions: University of California, San Francisco (United States)
Journal: Imaging neuroscience (Cambridge, Mass.), volume 4, article IMAG.a.1294
Dates: received 8 October 2025; accepted 8 June 2026; published online 8 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1162/imag.a.1294 · PMID 42428552 · PMCID PMC13347599 · OpenAlex W7165785340
Open access: diamond, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), other (modality), cellular / molecular (subfield)
Methods: Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, fMRI & imaging
Keywords: low-intensity focused ultrasound, transcranial ultrasound stimulation, ultrasound parameters, genetic mechanisms
Topic: Ultrasound and Hyperthermia Applications (Biomedical Engineering, Engineering), according to OpenAlex
Funding: Tianqiao & Chrissy Chen Institute; Marcus Innovation Fund; UCSF Resource Allocation Program; Pritzker Family Foundation
Citations: not cited yet (Europe PMC); 64 references in the paper

Abstract

Non-invasive transcranial ultrasound stimulation (TUS) enables deep brain therapeutic exploration at unprecedented scale. However, its optimal use is limited by the uncertainty surrounding ultrasound sensitivity across brain regions and cell types. This uncertainty often forces selection of sub-optimal parameter–target treatment paradigms guided solely by precedent, rather than an unbiased search of the larger combinatorial space. In principle, human brain-wide gene expression data could allow for a refinement of the search space based on known mechanisms and associated gene expression. In this perspective, we discuss the practicality of genetically informed TUS parameter search in humans using the Allen Brain Atlas and incorporating a broad set of genes related to the hypothesized mechanisms of TUS neuromodulation. We define principal component expression patterns across the brain, enabling dimensionality reduction and spatial clustering of ultrasound-relevant gene expression data. We identify regional clusters of covarying gene expression profiles across the brain topology that are likely to have similar responsivity to TUS. These findings may explain previous perplexities around highly variant neuronal response across brain areas and highlight the need to optimize stimulation parameters in the context of brain region and its molecular profile.

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

Repositories

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

jolinefan/TUSgeneClusters

License: none: the authors keep all their rights
State: the link is dead, verified on 27 September 2026
Evidence: found in the paper
Software Heritage: not archived
Found in: “Data and Code Availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link is dead
  • 27 September 2026: the link is dead

jolinefan/TUSgeneClustering

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 9ef7e4d322422a4531f1305659e5a3dd061e6f6c, 1 July 2026
Languages: Python (18)
Size: 41 files, 18 scripts
Software Heritage: not archived
Found in: “Data and Code Availability”
Holds: README, environment (requirements.txt, pubMed/requirements.txt), tests
Not found: license file, CITATION.cff, continuous integration, documentation
Tools: NumPy (11 files), pandas (8 files), Matplotlib (4 files), scikit-learn (4 files), SciPy (4 files), NiBabel (1 file), Nilearn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
19 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:

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

Code is available at https://github.com/jolinefan/TUSgeneClustering.git (https://github.com/jolinefan/TUSgeneClusters.git)

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, 4 authors, 4 keywords, 4 funders, 64 references.

Cite

This paper

Fan, J. M., Tadayon, E., Krystal, A. D., & Murphy, K. R. (2026). Toward a transcriptomic framework for ultrasound neuromodulation: A perspective on gene expression and regional brain sensitivity. Imaging neuroscience (Cambridge, Mass.), 4, IMAG.a.1294. https://doi.org/10.1162/imag.a.1294

BibTeX

@article{fan2026toward,
author = {Fan, Joline M. and Tadayon, Ehsan and Krystal, Andrew D. and Murphy, Keith R.},
title = {{Toward a transcriptomic framework for ultrasound neuromodulation: A perspective on gene expression and regional brain sensitivity}},
journal = {Imaging neuroscience (Cambridge, Mass.)},
year = {2026},
month = jul,
volume = {4},
pages = {IMAG.a.1294},
publisher = {MIT Press},
issn = {2837-6056},
doi = {10.1162/imag.a.1294},
url = {https://doi.org/10.1162/imag.a.1294},
pmid = {42428552},
pmcid = {PMC13347599}
}

RIS

TY - JOUR
AU - Fan, Joline M.
AU - Tadayon, Ehsan
AU - Krystal, Andrew D.
AU - Murphy, Keith R.
TI - Toward a transcriptomic framework for ultrasound neuromodulation: A perspective on gene expression and regional brain sensitivity
T2 - Imaging neuroscience (Cambridge, Mass.)
J2 - Imaging Neurosci (Camb)
PY - 2026
DA - 2026/07/08
VL - 4
SP - IMAG.a.1294
SN - 2837-6056
PB - MIT Press
DO - 10.1162/imag.a.1294
UR - https://doi.org/10.1162/imag.a.1294
LA - en
ER -

CSL-JSON

{
"id": "10.1162/imag.a.1294",
"type": "article-journal",
"title": "Toward a transcriptomic framework for ultrasound neuromodulation: A perspective on gene expression and regional brain sensitivity",
"container-title": "Imaging neuroscience (Cambridge, Mass.)",
"author": [
{
"family": "Fan",
"given": "Joline M."
},
{
"family": "Tadayon",
"given": "Ehsan"
},
{
"family": "Krystal",
"given": "Andrew D."
},
{
"family": "Murphy",
"given": "Keith R."
}
],
"container-title-short": "Imaging Neurosci (Camb)",
"volume": "4",
"page": "IMAG.a.1294",
"DOI": "10.1162/imag.a.1294",
"PMID": "42428552",
"PMCID": "PMC13347599",
"ISSN": "2837-6056",
"publisher": "MIT Press",
"URL": "https://doi.org/10.1162/imag.a.1294",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
8
]
]
}
}

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-75826-8
Transcranial focused ultrasound modulates spiking, LFP, and BOLD activity in the primate thalamus.
Journal: Nature communications
In common: other, 11 references
[2] doi:10.1038/s41467-026-74779-2 [code]
TRPC4/TRPC5 are critical for neuronal modulation by transcranial focused ultrasound in retrosplenial cortex in male mice.
Journal: Nature communications
In common: other, cellular / molecular, 8 references
[3] doi:10.1038/s41467-026-73826-2 [code]
Non-invasive in vivo acoustoelectric neuromodulation and its contribution to ultrasound stimulation.
Journal: Nature communications
In common: statsmodels, scikit-learn, pandas, 3 other tools, other, 4 references
[4] doi:10.1038/s41378-026-01391-1
High pressure transcranial focused ultrasound stimulation induces parameter-dependent cell-type specific effects.
Journal: Microsystems & nanoengineering
In common: other, 7 references
[5] 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, statsmodels, NiBabel, 5 other tools, cellular / molecular, 3 references
[6] doi:10.1038/s42003-025-09444-3 [code]
Decoupling of neurophysiological activity from structure mirrors global microarchitectural and neuromodulatory trends.
Journal: Communications biology
In common: Nilearn, NiBabel, scikit-learn, 4 other tools, cellular / molecular, 4 references
[7] doi:10.1038/s41531-026-01354-3 [code]
Neuromodulation-induced normalization of cortical metastable dynamics signatures in Parkinson's disease.
Journal: NPJ Parkinson's disease
In common: Nilearn, statsmodels, NiBabel, 5 other tools, 3 references
[8] doi:10.1038/s41467-026-71923-w [code]
Integrating optogenetic fMRI and spatial transcriptomics to reveal circuit-specific gene signatures in fronto- and hippo-thalamic networks.
Journal: Nature communications
In common: Nilearn, NiBabel, scikit-learn, 4 other tools, genetics / omics, 3 references
[9] doi:10.1162/imag.a.1282 [code]
Metabolic syndrome severity and the energetic cost of brain network transitions: A normative modeling study of accelerated brain aging.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, statsmodels, NiBabel, 5 other tools, 2 references
[10] 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: Nilearn, statsmodels, NiBabel, 5 other tools, 2 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.