OSCR

Decoding the role of transcriptomic clocks in the human prefrontal cortex.

Code ↔ Paper

24 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 24 matches
  1. [1] § STAR★Methods › Method details › Functional analyses › MENTOR network embedding of functional gene connections ↔ 10other-07302025/03plots-08272025/00plots-08282025.ipynb, lines 139–234 · score 0.96 · deMagalhaes, DESeq2, StocH, StocP, StocZ, GenAge
  2. [2] § STAR★Methods › Method details › Stochastic modeling of transcriptomic clocks ↔ 09stochastic-08172025/0stochastic-0817025.ipynb, lines 424–463 · score 0.92 · small jitter, standard deviations, reduce perfect correlations, multivariate normal, covariance matrices, gene correlation
  3. [3] § STAR★Methods › Method details › Transcriptomic clocks analyses ↔ 10other-07302025/04plots-0828205/00plots-08282025.ipynb, lines 79–100 · score 0.89 · de Magalhaes, DESeq2, GenAge, RNA seq, GTExAge, brain tissue
  4. [4] § STAR★Methods › Method details › Transcriptomic clocks analyses ↔ 11functional-08202025/00functional_analysis-08202025.ipynb, lines 78–97 · score 0.89 · de Magalhaes, DESeq2, GenAge, RNA seq, GTExAge, brain tissue
  5. [5] § STAR★Methods › Method details › Deep learning age prediction ↔ 08training-08152025/01deeplearning/00deeplearnin-08152025.ipynb, lines 271–312 · score 0.82 · dropout rate, Keras, Glorot, loss, Adam, MAE
  6. [6] § STAR★Methods › Method details › Deep learning age prediction ↔ 09stochastic-08172025/0stochastic-0817025.ipynb, lines 946–987 · score 0.82 · dropout rate, Keras, Glorot, loss, Adam, MAE
  7. [7] § STAR★Methods › Method details › Elastic net age prediction ↔ 08training-08152025/00elasticnet/00elasticnet-08152025.ipynb, lines 374–413 · score 0.79 · ElasticNetCV, l1_ratio, fold predictions, split, metrics, scores
  8. [8] § STAR★Methods › Method details › Elastic net age prediction ↔ 09stochastic-08172025/0stochastic-0817025.ipynb, lines 786–825 · score 0.78 · ElasticNetCV, l1_ratio, fold predictions, split, metrics, scores
  9. [9] § STAR★Methods › Method details › Functional analyses › MENTOR network embedding of functional gene connections ↔ src/mentor/cli.py, lines 195–340 · score 0.74 · pairwise distance, MENTOR, Spearman, linkage, singleton, subclustering
  10. [10] § Results › Comparison of transcriptomic clocks and epigenetic clocks ↔ 10other-07302025/03plots-08272025/00plots-08282025.ipynb, lines 139–234 · score 0.73 · DamAge, PhenoAge, RNAAgeCalc, retrained transcriptomic clock, deep learning, Epigenetic clocks
  11. [11] § STAR★Methods › Method details › Gene annotation of epigenetic clock ↔ 10other-07302025/00plots-07302025/01rnaagecalc-08032025.ipynb, lines 264–321 · score 0.71 · StocH, StocP, StocZ, PhenoAge, epigenetic clock, Zhang2019
  12. [12] § Results › Performance of the transcriptomic clocks in predicting age ↔ 01GEOcohorts/01rnagecalc-08052025.R, lines 50–103 · score 0.69 · DESeq2, GenAge, GTExAge, RNAAgeCalc, Dev, Peters
  13. [13] § Results › Performance of the transcriptomic clocks in predicting age ↔ 01ragecalc-07272025/00rnagecalc-07272025.R, lines 41–106 · score 0.69 · DESeq2, GenAge, GTExAge, RNAAgeCalc, Dev, Peters
  14. [14] § Results › Stochastic effects of the transcriptomic clocks ↔ 10other-07302025/02plots-08272025/00plots-08272025.ipynb, lines 256–358 · score 0.65 · R2 stochastic, R2 deterministic, variance explained, deep learning, RR2, Elastic
  15. [15] § STAR★Methods › Experimental model and study participant details › University of Texas health science center at Houston (UTHealth) brain collection ↔ 07assoc-08052025/00uthealth-08052025.ipynb, lines 180–214 · score 0.65 · consensus diagnoses, UTHealth Brain, autopsy, ethnicity, drug, alcohol
  16. [16] § STAR★Methods › Experimental model and study participant details › University of Texas health science center at Houston (UTHealth) brain collection ↔ 07assoc-08052025/02uthealth-08282025.ipynb, lines 195–229 · score 0.65 · consensus diagnoses, UTHealth Brain, autopsy, ethnicity, drug, alcohol
  17. [17] § Results › Associations of transcriptomic age with psychiatric-related phenotypes ↔ 07assoc-08052025/04vabb-09012025.ipynb, lines 388–467 · score 0.64 · anticonvulsants, antidepressants, hallucinogen, polysubstance, antipsychotics, lifetime
  18. [18] § Results › Associations of transcriptomic age with psychiatric-related phenotypes ↔ 07assoc-08052025/01vabb-08062025.ipynb, lines 198–290 · score 0.63 · anticonvulsants, antidepressants, hallucinogen, polysubstance, antipsychotics, lifetime
  19. [19] § STAR★Methods › Quantification and statistical analysis ↔ 10other-07302025/01plots-08242025/00plots-08242025.ipynb, lines 618–742 · score 0.60 · squared error, Pearson correlation, Correlation matrices, R2, RMSE, retrained
  20. [20] § STAR★Methods › Quantification and statistical analysis ↔ 10other-07302025/02plots-08272025/00plots-08272025.ipynb, lines 256–358 · score 0.56 · variance explained, Pearson correlation, RR2, squared, stochastic, Prediction
  21. [21] § Results › Performance of transcriptomic clocks in predicting delta of age ↔ 10other-07302025/01plots-08242025/01plots-08262025.ipynb, lines 508–585 · score 0.55 · concordant decrease, delta age, density, deep learning, retrained, elastic
  22. [22] § STAR★Methods › Method details › Retraining transcriptomic clocks ↔ 08training-08152025/00elasticnet/00elasticnet-08152025.ipynb, lines 252–306 · score 0.54 · Benjamini Hochberg, FDR, sex, fit, models, gene
  23. [23] § STAR★Methods › Method details › DNA methylation analysis ↔ 11functional-08202025/01CpG2genelistepigenetics-08202025.R, lines 1–71 · score 0.53 · Human Methylation, EPIC, San, Illumina
  24. [24] § Results › Comparison of transcriptomic clocks and epigenetic clocks ↔ 10other-07302025/00plots-07302025/01rnaagecalc-08032025.ipynb, lines 264–321 · score 0.51 · DamAge, PhenoAge, Epigenetic clocks, cortical, Zhang2019, transcriptomic clock

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,144 lines · 38 KB · no license · 3 matches

  1. # %%
  2. import os, random
  3. import re
  4. import pyreadr
  5. import numpy as np
  6. import pandas as pd
  7. import warnings
  8. import optuna
  9. from scipy import stats
  10. import tensorflow as tf
  11. from typing import List, Tuple
  12. from sklearn.decomposition import PCA
  13. from sklearn.preprocessing import StandardScaler
  14. from sklearn.exceptions import ConvergenceWarning
  15. from scipy.interpolate import interp1d
  16. from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
  17. from sklearn.model_selection import KFold, cross_val_score
  18. from sklearn.linear_model import ElasticNetCV
  19. import statsmodels.api as sm
  20. import seaborn as sns
  21. import matplotlib.pyplot as plt
  22. from sklearn.covariance import LedoitWolf
  23. from statsmodels.tools.sm_exceptions import ConvergenceWarning, PerfectSeparationWarning
  24. from tensorflow.keras.models import Sequential
  25. from tensorflow.keras.models import load_model
  26. from tensorflow.keras import losses # For mse
  27. from tensorflow.keras.losses import MeanSquaredError
  28. from tensorflow.keras.layers import Dense, Dropout
  29. from tensorflow.keras.optimizers import Adam
  30. from tensorflow.keras.callbacks import EarlyStopping
  31. from sklearn.model_selection import train_test_split
  32. from sklearn.feature_selection import SelectFromModel
  33. from tensorflow.keras.models import load_model
  34. import tensorflow.keras.metrics as metrics
  35. import matplotlib.pyplot as plt
  36. from tensorflow.keras.models import Sequential
  37. from tensorflow.keras.layers import Dense, Dropout
  38. from tensorflow.keras.optimizers import Adam
  39. from tensorflow.keras.initializers import GlorotUniform
  40. # Shutdown warnings
  41. warnings.simplefilter("ignore", ConvergenceWarning)
  42. warnings.simplefilter("ignore", PerfectSeparationWarning)
  43. # Suppress all sklearn convergence warnings globally
  44. warnings.filterwarnings("ignore", category=ConvergenceWarning)
  45. warnings.filterwarnings("ignore", category=RuntimeWarning)
  46. # %%
  47. # -----------------------------------------------------------------
  48. # Set working directory
  49. wkdir = r"C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/00databases/01transcriptome/00data/"
  50. os.chdir(wkdir)
  51. # -----------------------------------------------------------------
  52. # File paths
  53. #uthbCounts = r"00uthb_genecounts-08152025.csv.gz"
  54. vabbCounts = r"00vabb_genecounts-08152025.csv.gz"
  55. # Reading annotation
  56. #uthbAnnot = r"00uthb_annotation-08152025.csv.gz"
  57. vabbAnnot = r"00vabb_annotation-08152025.csv.gz"
  58. # -----------------------------------------------------------------
  59. # Read gzipped CSVs
  60. #expr_uthb = pd.read_csv(uthbCounts, index_col=0, compression='gzip')
  61. expr_vabb = pd.read_csv(vabbCounts, index_col=0, compression='gzip')
  62. #annot_uthb = pd.read_csv(uthbAnnot, index_col=0, compression='gzip')
  63. annot_vabb = pd.read_csv(vabbAnnot, index_col=0, compression='gzip')
  64. # -----------------------------------------------------------------
  65. # %%
  66. # -----------------------------------------------------------------
  67. # Loading metadata
  68. # loading phenotype data
  69. phenoPath = "C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/00databases/00phenotype/00vabb/00PhenoMeta_trainclock-07272025.csv"
  70. phenoData = pd.read_csv(phenoPath)
  71. # Create suffixes
  72. suffixes = ["_9", "_24", "_25", "_11"]
  73. # Replicate and modify pheno_data
  74. phenoData = pd.concat([
  75. phenoData.assign(SampleID="Sample" + phenoData["SampleID"].astype(str) + suffix)
  76. for suffix in suffixes
  77. ], ignore_index=True)
  78. phenoData.shape
  79. # -----------------------------------------------------------------
  80. # %%
  81. # Load cell types and SVAs
  82. cells = pd.read_csv("C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/05celltype-08042025/00vabb_cellprop_svas-08042025.csv")
  83. cells = cells.rename(columns={'name': 'SampleID'})
  84. cells.shape
  85. # %%
  86. # Merge in sequence after conversion
  87. merged_df_com = pd.merge(phenoData, cells, on='SampleID')
  88. meta = merged_df_com
  89. meta.shape
  90. # %%
  91. # -----------------------------------------------------------------
  92. # Subset meta to only required columns
  93. meta = meta[['SampleID', 'Sex', 'PMI', 'RIN', 'AgeDeath', 'ast', 'end', 'mic', 'neu', 'oli', 'opc', 'W_1', 'W_2']]
  94. # Columns not to scale
  95. exclude_cols = ['SampleID', 'Sex', 'AgeDeath']
  96. # Identify columns to scale
  97. cols_to_scale = [col for col in meta.columns if col not in exclude_cols]
  98. # Initialize scaler
  99. scaler = StandardScaler(with_mean=True, with_std=True)
  100. # Scale selected columns
  101. meta_scaled = meta.copy()
  102. meta_scaled[cols_to_scale] = scaler.fit_transform(meta_scaled[cols_to_scale])
  103. # Set SampleID as index
  104. meta_scaled = meta_scaled.set_index('SampleID')
  105. meta_scaled.head()
  106. # %%
  107. # -----------------------------------------------------------------
  108. # Function to clean sample names
  109. def clean_sample_name(name):
  110. # Match "Sample<number><letters><number>" and replace with "Sample<number>_<number>"
  111. return re.sub(r"(Sample\d+)[A-Za-z]+(\d+)", r"\1_\2", name)
  112. # Apply to a DataFrame
  113. # Example: expr_uthb.columns = [clean_sample_name(c) for c in expr_uthb.columns]
  114. expr_vabb.columns = [clean_sample_name(c) for c in expr_vabb.columns]
  115. expr_vabb.head()
  116. # -----------------------------------------------------------------
  117. # %%
  118. # -----------------------------------------------------------------
  119. # Define a function for the estimation FPKM of transcriptome data
  120. def counts_to_fpkm(counts: pd.DataFrame, gene_lengths: pd.Series, lengths_in: str = "bp") -> pd.DataFrame:
  121. """
  122. Convert raw counts to FPKM.
  123. counts: genes x samples
  124. gene_lengths: length per gene (index aligned to counts)
  125. lengths_in: 'bp' or 'kb'
  126. """
  127. assert set(counts.index).issubset(set(gene_lengths.index)), "Gene lengths missing for some genes"
  128. # Convert lengths to kilobases
  129. L = gene_lengths.loc[counts.index].astype(float)
  130. if lengths_in == "bp":
  131. L = L / 1e3
  132. elif lengths_in != "kb":
  133. raise ValueError("lengths_in must be 'bp' or 'kb'")
  134. # RPK: counts / length_kb
  135. rpk = counts.divide(L, axis=0)
  136. # FPKM: RPK / (total_mapped_reads_in_millions)
  137. per_sample_scaler = counts.sum(axis=0) / 1e6
  138. fpkm = rpk.divide(per_sample_scaler, axis=1)
  139. return fpkm
  140. # -----------------------------------------------------------------
  141. # Define a function for the estimation of gene lengths
  142. def extract_gene_bounds(df: pd.DataFrame, start_col='Start', end_col='End'):
  143. """
  144. Extract first start and last end for genes where Start/End columns may contain multiple positions separated by ';'.
  145. Args:
  146. df: DataFrame with at least 'GeneID', start_col, end_col
  147. start_col: name of the start column
  148. end_col: name of the end column
  149. Returns:
  150. DataFrame with 'GeneID', 'Start', 'End', 'GeneLength'
  151. """
  152. starts = []
  153. ends = []
  154. for s, e in zip(df[start_col], df[end_col]):
  155. # Split by ',' and convert to int
  156. start_vals = [int(x) for x in str(s).split(';') if x.strip().isdigit()]
  157. end_vals = [int(x) for x in str(e).split(';') if x.strip().isdigit()]
  158. if start_vals and end_vals:
  159. starts.append(min(start_vals)) # first start = smallest
  160. ends.append(max(end_vals)) # last end = largest
  161. else:
  162. starts.append(None)
  163. ends.append(None)
  164. result = pd.DataFrame({
  165. 'GeneID': df['GeneID'],
  166. 'Start': starts,
  167. 'End': ends
  168. })
  169. # Compute gene length
  170. result['GeneLength'] = result['End'] - result['Start'] + 1
  171. return result
  172. # %%
  173. # -----------------------------------------------------------------
  174. # Transforming to FPKM
  175. gene_info_vabb = extract_gene_bounds(annot_vabb)
  176. # Make Series: index = GeneID, values = GeneLength
  177. gene_lengths = pd.Series(gene_info_vabb["GeneLength"].values, index=gene_info_vabb["GeneID"])
  178. fpkm_matrix = counts_to_fpkm(expr_vabb, gene_lengths, lengths_in="bp")
  179. fpkm_matrix.shape
  180. # %%
  181. # -----------------------------------------------------------------
  182. # Filtering to FPKM
  183. # To avoid the influence of low count genes on the analysis result, genes with more than 30% samples having count per million (CPM) less than one were filtered out
  184. mfilter = (fpkm_matrix < 1).sum(axis=1) / fpkm_matrix.shape[1] <= 0.3
  185. fpkm_matrix = fpkm_matrix.loc[mfilter] # assign back
  186. fpkm_matrix.shape
  187. # %%
  188. # -----------------------------------------------------------------
  189. # Standardize by gene (rows)
  190. scaler = StandardScaler(with_mean=True, with_std=True)
  191. expr_z = pd.DataFrame(
  192. scaler.fit_transform(fpkm_matrix.T).T, # transpose -> scale -> transpose back
  193. index=fpkm_matrix.index, # keep gene names
  194. columns=fpkm_matrix.columns # keep sample names
  195. )
  196. expr_z.head()
  197. # %%
  198. # -----------------------------------------------------------------
  199. # Subsetting to only unique samples
  200. # Example: columns in your expression matrix
  201. columns = expr_z.columns # expr is your genes x samples DataFrame
  202. # Extract the prefix before the underscore
  203. prefixes = [col.split('_')[0] for col in columns]
  204. # Create a DataFrame to associate columns with prefixes
  205. col_df = pd.DataFrame({'col': columns, 'prefix': prefixes})
  206. # Set random seed for reproducibility
  207. seed = 42
  208. np.random.seed(seed)
  209. # Sample one column per prefix
  210. unique_cols = col_df.groupby('prefix')['col'].apply(lambda x: np.random.choice(x)).values
  211. # Subset your expression matrix
  212. expr_unique = expr_z[unique_cols]
  213. # Check
  214. print(expr_unique.shape)
  215. print(expr_z.shape)
  216. # %%
  217. # -----------------------------------------------------------------
  218. # Columns that were selected
  219. unique_cols_set = set(unique_cols)
  220. # Columns that were not selected
  221. excluded_cols = [col for col in expr_z.columns if col not in unique_cols_set]
  222. # Check
  223. print("Number of selected columns:", len(unique_cols))
  224. print("Number of excluded columns:", len(excluded_cols))
  225. # Optional: print first 10 excluded columns
  226. print("Excluded columns:", excluded_cols[:10])
  227. # %%
  228. # ------------------------------
  229. # Align samples
  230. common_samples = expr_unique.columns.intersection(meta_scaled.index)
  231. expr_unique = expr_unique[common_samples]
  232. meta_scaled2 = meta_scaled.loc[common_samples]
  233. print(expr_unique.shape)
  234. print(meta_scaled2.shape)
  235. # Plotting Age
  236. plt.hist(meta_scaled2['AgeDeath'], bins=30, edgecolor='black')
  237. plt.xlabel("Age")
  238. plt.ylabel("Count")
  239. plt.title("Age distribution")
  240. plt.show()
  241. # %%
  242. # ------------------------------
  243. # Keep only samples <30 or >60
  244. meta_subset = meta_scaled2[(meta_scaled2['AgeDeath'] < 30) | (meta_scaled2['AgeDeath'] > 60)].copy()
  245. # Add new variable: 0 = young (<30), 1 = old (>60)
  246. meta_subset['age_group'] = (meta_subset['AgeDeath'] > 60).astype(int)
  247. # Subset expression matrix to the same samples
  248. expr_subset = expr_unique[meta_subset.index]
  249. print(meta_subset.shape, expr_subset.shape)
  250. meta_subset[['AgeDeath', 'age_group']].head()
  251. # %%
  252. # ------------------------------
  253. # Keep only genes in clock
  254. coefPath = "C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/06tranningclock-08152025/00elasticnet-08152025/02elastic_net_nonzero_coefficients-08152025.csv"
  255. # Load coefficients
  256. df_coef = pd.read_csv(coefPath, index_col=0)
  257. # Convert the single-column DataFrame to a Series
  258. coef_series = pd.Series(df_coef['coef'].values, index=df_coef.index, name='coef')
  259. # Ensure it's a Series
  260. coef_series = pd.Series(coef_series, name='coef')
  261. coef = df_coef
  262. genes_to_keep = coef.index
  263. expr_subset = expr_subset.loc[genes_to_keep]
  264. expr_subset.shape
  265. # %%
  266. # ------------------------------
  267. # Prepare data
  268. # ------------------------------
  269. # Keep covariates of interest
  270. covariates = meta_subset[['Sex', 'PMI', 'RIN']].copy()
  271. # One-hot encode Sex if necessary
  272. covariates = pd.get_dummies(covariates, columns=['Sex'], drop_first=True)
  273. # Add intercept
  274. Xcov_only = sm.add_constant(covariates).astype(float)
  275. # Outcome variable: 0 = young, 1 = old
  276. y = meta_subset['age_group'].values
  277. # Align samples with expression matrix
  278. expr_use = expr_subset
  279. common_samples = expr_use.columns.intersection(meta_subset.index)
  280. expr_use = expr_use[common_samples]
  281. Xcov_only = Xcov_only.loc[common_samples]
  282. y = y[np.isin(meta_subset.index, common_samples)]
  283. # ------------------------------
  284. # Fit null logistic model
  285. # ------------------------------
  286. null_model = sm.Logit(y, Xcov_only).fit(disp=0)
  287. p_hat = null_model.predict()
  288. # W matrix: diagonal of p*(1-p)
  289. W = np.diag(p_hat * (1 - p_hat))
  290. # ------------------------------
  291. # Compute z-scores for each gene
  292. # ------------------------------
  293. results = []
  294. for gene_id in expr_use.index:
  295. x = expr_use.loc[gene_id].values.reshape(-1,1) # already standardized
  296. # If gene has zero variance, set z=0
  297. if np.var(x) == 0:
  298. results.append((gene_id, 0.0))
  299. continue
  300. # Wald z-score: z = (x^T (y - p_hat)) / sqrt(x^T W x)
  301. numerator = x.T @ (y - p_hat)
  302. denominator = np.sqrt(x.T @ W @ x)
  303. z = float(numerator / denominator)
  304. results.append((gene_id, z))
  305. # ------------------------------
  306. # Compile results
  307. # ------------------------------
  308. deg_z = pd.DataFrame(results, columns=['gene_id','z_expr']).set_index('gene_id')
  309. deg_z.head()
  310. # %%
  311. # Plot histogram of the first few genes (deg_z.head())
  312. plt.figure(figsize=(6,4))
  313. plt.hist(deg_z['z_expr'], bins=10, color='skyblue', edgecolor='black')
  314. plt.xlabel('Z-score')
  315. plt.ylabel('Frequency')
  316. plt.title('Histogram of Z-scores')
  317. plt.show()
  318. # %%
  319. # ------------------------------
  320. # Start stochastic modelling
  321. # ------------------------------
  322. # Define helper functions
  323. def cov_from_corr_sd(corr: pd.DataFrame, sd: pd.Series) -> pd.DataFrame:
  324. """Covariance = D * R * D, with D=diag(sd)."""
  325. D = np.diag(sd.values)
  326. cov = D @ corr.values @ D
  327. return pd.DataFrame(cov, index=corr.index, columns=corr.columns)
  328. def shrink_corr_from_matrix(X: pd.DataFrame) -> pd.DataFrame:
  329. """
  330. Estimate correlation via Ledoit-Wolf shrinkage
  331. from an expression matrix (samples x genes).
  332. X = samples x genes
  333. """
  334. Xc = X - X.mean(axis=0)
  335. lw = LedoitWolf().fit(Xc.values)
  336. cov = pd.DataFrame(lw.covariance_, index=X.columns, columns=X.columns)
  337. d = np.sqrt(np.diag(cov.values))
  338. corr = cov / d[:, None] / d[None, :]
  339. return corr
  340. # %%
  341. # ------------------------------
  342. # Convert z-scores → age-specific means
  343. # ------------------------------
  344. def z_to_means(genes, z_age80_vs_25, space="z",
  345. baseline_mean_age25=None, baseline_sd_age25=None, se_per_gene=None):
  346. """
  347. space = "z": assume standardized space
  348. - age 25 mean = 0, sd = 1
  349. - age 80 mean = z
  350. space = "raw": use delta = z * SE
  351. """
  352. z_age80_vs_25 = z_age80_vs_25.reindex(genes)
  353. if space == "z":
  354. mean25 = pd.Series(0.0, index=genes)
  355. sd25 = pd.Series(1.0, index=genes) if baseline_sd_age25 is None else baseline_sd_age25.reindex(genes)
  356. mean80 = z_age80_vs_25.copy()
  357. sd80 = sd25.copy()
  358. elif space == "raw":
  359. if se_per_gene is None or baseline_mean_age25 is None:
  360. raise ValueError("Need baseline_mean_age25 and se_per_gene for raw mode")
  361. mean25 = baseline_mean_age25.reindex(genes)
  362. sd25 = pd.Series(1.0, index=genes) if baseline_sd_age25 is None else baseline_sd_age25.reindex(genes)
  363. delta = z_age80_vs_25 * se_per_gene.reindex(genes)
  364. mean80 = mean25 + delta
  365. sd80 = sd25
  366. else:
  367. raise ValueError("space must be 'z' or 'raw'")
  368. return mean25, sd25, mean80, sd80
  369. # %%
  370. # ------------------------------
  371. # Adjusted MVN simulation to reduce excessive collinearity
  372. # ------------------------------
  373. def simulate_mvn(n, mean, corr=None, sd=None, jitter=1e-3):
  374. """
  375. Simulate multivariate normal samples for genes with slight jitter to reduce
  376. perfect correlations for better convergence in ElasticNet.
  377. Parameters
  378. ----------
  379. n : int
  380. Number of samples.
  381. mean : pd.Series
  382. Mean per gene.
  383. corr : pd.DataFrame, optional
  384. Gene-gene correlation matrix.
  385. sd : pd.Series, optional
  386. Standard deviation per gene.
  387. jitter : float, optional
  388. Small value to add to diagonal of covariance to reduce collinearity.
  389. """
  390. genes = mean.index
  391. p = len(genes)
  392. # Default identity correlation / unit SD
  393. if corr is None:
  394. corr = pd.DataFrame(np.eye(p), index=genes, columns=genes)
  395. if sd is None:
  396. sd = pd.Series(1.0, index=genes)
  397. # Covariance matrix from correlation and SD
  398. cov = cov_from_corr_sd(corr, sd)
  399. # Add small jitter to diagonal to reduce perfect correlation
  400. cov += np.eye(p) * jitter
  401. # Draw samples
  402. X = np.random.multivariate_normal(mean.values, cov.values, size=n)
  403. return pd.DataFrame(X, columns=genes)
  404. # %%
  405. # ------------------------------
  406. # Define a Multivariate Normal simulation
  407. # ------------------------------
  408. def negbin_params_from_mean_k(mu, k):
  409. n_param = k
  410. p_param = k / (k + mu)
  411. return n_param, p_param
  412. def gaussian_copula_transform(n, corr):
  413. R = corr.values
  414. Z = np.random.multivariate_normal(mean=np.zeros(len(R)), cov=R, size=n)
  415. U = stats.norm.cdf(Z)
  416. return U
  417. def simulate_gaussian_copula_negbin(n, corr, mean_counts, k_overdisp):
  418. genes = mean_counts.index
  419. U = gaussian_copula_transform(n, corr)
  420. out = np.empty_like(U, dtype=np.int64)
  421. for j, g in enumerate(genes):
  422. mu = float(mean_counts[g])
  423. k = float(k_overdisp[g])
  424. n_param, p_param = negbin_params_from_mean_k(mu, k)
  425. out[:, j] = stats.nbinom.ppf(U[:, j], n=n_param, p=p_param).astype(int)
  426. return pd.DataFrame(out, columns=genes)
  427. # %%
  428. # ------------------------------
  429. # Running simulations for ages 20 and 60
  430. # ------------------------------
  431. # This generates
  432. genes = deg_z.index
  433. z = deg_z
  434. # Convert z-scores → age-specific means in z-space
  435. mean20, sd20, mean60, sd60 = z_to_means(genes, z, space="z")
  436. # Correlation of genes
  437. expr_use_T = expr_use.T # shape = (samples, genes)
  438. corr = shrink_corr_from_matrix(expr_use_T)
  439. # Ensure 1D Series and alignment
  440. mean20 = mean20.squeeze()
  441. mean60 = mean60.squeeze()
  442. sd20 = sd20.squeeze()
  443. sd60 = sd60.squeeze()
  444. genes_common = mean20.index.intersection(corr.index)
  445. mean20 = mean20.loc[genes_common]
  446. mean60 = mean60.loc[genes_common]
  447. sd20 = sd20.loc[genes_common]
  448. sd60 = sd60.loc[genes_common]
  449. corr = corr.loc[genes_common, genes_common]
  450. # ---- MVN samples ----
  451. samples20 = simulate_mvn(2000, mean20, corr=corr, sd=sd20)
  452. samples60 = simulate_mvn(2000, mean60, corr=corr, sd=sd60)
  453. samples20["age"] = 20
  454. samples60["age"] = 60
  455. mvn_samples = pd.concat([samples20, samples60], ignore_index=True)
  456. mvn_samples.head()
  457. # %%
  458. # ------------------------------
  459. # Running Gaussian Copula–NB simulations for ages 20 and 60
  460. # ------------------------------
  461. # This generates COUNTS
  462. # If z is DataFrame with one column
  463. z = z.squeeze() # now a Series
  464. # Example: map z-scores to mean counts (toy example)
  465. # You can adjust this to your real counts
  466. baseline_mu = pd.Series(100.0, index=genes_common) # baseline counts
  467. fold_change = np.exp(z[genes_common] * 0.2) # simple mapping z -> fold change
  468. mu20 = baseline_mu # age 20
  469. mu60 = baseline_mu * fold_change # age 60
  470. # Overdispersion parameter k (can be gene-specific)
  471. k20 = pd.Series(10.0, index=genes_common)
  472. k60 = pd.Series(10.0, index=genes_common)
  473. # Simulate
  474. samples20_nb = simulate_gaussian_copula_negbin(2000, corr, mu20, k20)
  475. samples60_nb = simulate_gaussian_copula_negbin(2000, corr, mu60, k60)
  476. # Add age labels
  477. samples20_nb["age"] = 20
  478. samples60_nb["age"] = 60
  479. # Concatenate
  480. copula_nb_samples = pd.concat([samples20_nb, samples60_nb], ignore_index=True)
  481. copula_nb_samples.head()
  482. # %%
  483. # ------------------------------
  484. # PCA analysis of stochastic genes
  485. # Extract gene expression only
  486. X = mvn_samples.drop(columns=["age"]).values
  487. ages = mvn_samples["age"].values
  488. pca = PCA(n_components=2)
  489. pcs = pca.fit_transform(X)
  490. # Wrap into DataFrame
  491. pca_df = pd.DataFrame(pcs, columns=["PC1", "PC2"])
  492. pca_df["age"] = ages
  493. # Plotting
  494. plt.figure(figsize=(8,6))
  495. sns.scatterplot(data=pca_df, x="PC1", y="PC2", hue="age", palette="coolwarm", alpha=0.3)
  496. plt.title("PCA trajectory of ages 20 → 60")
  497. plt.xlabel(f"PC1 ({pca.explained_variance_ratio_[0]*100:.1f}% variance)")
  498. plt.ylabel(f"PC2 ({pca.explained_variance_ratio_[1]*100:.1f}% variance)")
  499. plt.show()
  500. # %%
  501. # ------------------------------
  502. # Compute mean expression per gene for each age group
  503. mean_expr = mvn_samples.groupby(['age']).mean().T # Transpose to have genes as rows
  504. mean_expr = mean_expr.loc[:, [20, 60]] # Select only ages 20 and 60
  505. # Scatter plots of stochastic genes
  506. plt.figure(figsize=(8, 6))
  507. # Draw a line for each gene
  508. for gene in mean_expr.index:
  509. plt.plot([20, 60], [mean_expr.loc[gene, 20], mean_expr.loc[gene, 60]],
  510. color='gray', alpha=0.5, lw=0.8)
  511. # Add points on top
  512. plt.scatter([20]*len(mean_expr), mean_expr[20], color='blue', label='Age 20')
  513. plt.scatter([60]*len(mean_expr), mean_expr[60], color='red', label='Age 60')
  514. plt.xlabel('Age')
  515. plt.ylabel('Mean Gene Expression')
  516. plt.title('Gene-level trajectories: Age 20 → 60')
  517. plt.legend()
  518. plt.grid(True)
  519. plt.show()
  520. # %%
  521. # ------------------------------
  522. # Scatter plots of expression per sample between ages 20 and 60
  523. # ------------------------------
  524. # ------------------------------
  525. # Scatter plots of expression per sample (no sample_id)
  526. # ------------------------------
  527. # Keep only age 20 and 60
  528. samples_20_60 = mvn_samples[mvn_samples['age'].isin([20, 60])]
  529. # Split by age
  530. samples_20 = samples_20_60[samples_20_60['age'] == 20].drop(columns=['age'])
  531. samples_60 = samples_20_60[samples_20_60['age'] == 60].drop(columns=['age'])
  532. plt.figure(figsize=(8, 6))
  533. # Loop over rows (each row = one sample’s expression vector)
  534. for i in range(min(len(samples_20), len(samples_60))):
  535. plt.plot([20, 60],
  536. [samples_20.iloc[i].mean(), samples_60.iloc[i].mean()],
  537. color='gray', alpha=0.5, lw=0.8)
  538. # Scatter points
  539. plt.scatter([20]*len(samples_20), samples_20.mean(axis=1),
  540. color='blue', label='Age 20')
  541. plt.scatter([60]*len(samples_60), samples_60.mean(axis=1),
  542. color='red', label='Age 60')
  543. plt.xlabel('Age')
  544. plt.ylabel('Sample-level Mean Gene Expression')
  545. plt.title('Sample-level trajectories: Age 20 → 60')
  546. plt.legend()
  547. plt.grid(True)
  548. plt.show()
  549. # %%
  550. # ------------------------------
  551. # Age range and interpolation
  552. # ------------------------------
  553. ages = [20, 30, 40, 50, 60]
  554. # Interpolate gene means for each age
  555. mean_df = pd.DataFrame(index=genes_common, columns=ages, dtype=float)
  556. for gene in genes_common:
  557. mean_df.loc[gene] = np.linspace(mean20[gene], mean60[gene], num=len(ages))
  558. # Optional: interpolate SDs across ages
  559. sd_df = pd.DataFrame(index=genes_common, columns=ages, dtype=float)
  560. for gene in genes_common:
  561. sd_df.loc[gene] = np.linspace(sd20[gene], sd60[gene], num=len(ages))
  562. # ------------------------------
  563. # Generate MVN samples for each age
  564. # ------------------------------
  565. n_samples_per_age = 2000
  566. all_samples = []
  567. for age in ages:
  568. mean_vec = mean_df[age].astype(float)
  569. sd_vec = sd_df[age].astype(float)
  570. samples_age = simulate_mvn(n_samples_per_age, mean_vec, corr=corr, sd=sd_vec)
  571. samples_age["age"] = age
  572. all_samples.append(samples_age)
  573. # Combine all ages
  574. mvn_samples_multi_age = pd.concat(all_samples, ignore_index=True)
  575. mvn_samples_multi_age.head()
  576. # %%
  577. # Compute mean expression per gene for each age group
  578. mean_expr = mvn_samples_multi_age.groupby('age').mean().T # genes as rows
  579. # Optional: select a subset of ages to plot
  580. ages_to_plot = [20, 30, 40, 50, 60]
  581. mean_expr = mean_expr.loc[:, ages_to_plot]
  582. # Plot
  583. plt.figure(figsize=(10, 6))
  584. # Draw a line for each gene
  585. for gene in mean_expr.index:
  586. plt.plot(ages_to_plot, mean_expr.loc[gene, ages_to_plot],
  587. color='gray', alpha=0.5, lw=0.8)
  588. # Add points on top
  589. for age in ages_to_plot:
  590. plt.scatter([age]*len(mean_expr), mean_expr[age],
  591. label=f'Age {age}' if age in [20, 60] else "", # label only 20 & 60
  592. s=20,
  593. color='blue' if age==20 else 'red' if age==60 else 'green')
  594. plt.xlabel('Age')
  595. plt.ylabel('Mean Gene Expression')
  596. plt.title('Gene-level trajectories: Age 20 → 60')
  597. plt.legend()
  598. plt.grid(True)
  599. plt.show()
  600. # %%
  601. # ------------------------------
  602. # Sample-level trajectories across ages
  603. # ------------------------------
  604. ages_to_plot = [20, 30, 40, 50, 60]
  605. samples_multi_age = mvn_samples_multi_age[mvn_samples_multi_age['age'].isin(ages_to_plot)]
  606. plt.figure(figsize=(10, 6))
  607. # Pivot: rows = sample index, cols = ages, values = mean expression
  608. sample_means = samples_multi_age.drop(columns=['age']).mean(axis=1) # per-sample mean
  609. sample_means = pd.DataFrame({
  610. "age": samples_multi_age['age'].values,
  611. "mean_expr": sample_means.values
  612. })
  613. # Group by index position (pseudo-IDs) across ages
  614. for idx in range(sample_means.shape[0] // len(ages_to_plot)):
  615. subset = sample_means.iloc[idx::(sample_means.shape[0] // len(ages_to_plot))]
  616. plt.plot(subset["age"], subset["mean_expr"], color="gray", alpha=0.5, lw=0.8)
  617. # Scatter points on top
  618. for age in ages_to_plot:
  619. subset = sample_means[sample_means['age'] == age]
  620. plt.scatter([age]*len(subset), subset['mean_expr'],
  621. color='blue' if age==20 else 'red' if age==60 else 'green',
  622. alpha=0.7, s=20, label=f'Age {age}' if age in [20, 60] else "")
  623. plt.xlabel('Age')
  624. plt.ylabel('Sample-level Mean Gene Expression')
  625. plt.title('Sample-level gene expression trajectories')
  626. plt.legend()
  627. plt.grid(True)
  628. plt.show()
  629. # %%
  630. # ------------------------------
  631. # Align samples
  632. # ------------------------------
  633. common_samples = expr_use.columns.intersection(meta_subset.index)
  634. expr_use_aligned = expr_use[common_samples]
  635. age_series = meta_subset.loc[common_samples, 'AgeDeath']
  636. # ------------------------------
  637. # Define age bins
  638. # ------------------------------
  639. bins = [20, 30, 40, 50, 60, 70, 80] # adjust as needed
  640. age_bins = pd.cut(age_series, bins=bins, right=False)
  641. # Compute mean expression per gene per age bin
  642. mean_expr = expr_use_aligned.T.groupby(age_bins).mean().T # genes x bins
  643. # Compute numeric bin centers
  644. bin_centers = [interval.left + (interval.right - interval.left)/2 for interval in mean_expr.columns]
  645. # Ensure numeric values and fill missing
  646. mean_expr_numeric = mean_expr.copy()
  647. mean_expr_numeric = mean_expr_numeric.astype(float).interpolate(axis=1) # fill NaNs if any
  648. # Plot
  649. plt.figure(figsize=(10, 6))
  650. for gene in mean_expr_numeric.index:
  651. y = mean_expr_numeric.loc[gene, :].values # numeric values across bins
  652. plt.plot(bin_centers, y, color='gray', alpha=0.5, lw=0.8)
  653. # Overlay points for each bin
  654. for i, center in enumerate(bin_centers):
  655. plt.scatter([center]*len(mean_expr_numeric),
  656. mean_expr_numeric.iloc[:, i].values,
  657. color='blue', s=15)
  658. plt.xlabel('Age')
  659. plt.ylabel('Mean Gene Expression')
  660. plt.title('Gene-level trajectories across AgeDeath')
  661. plt.grid(True)
  662. plt.show()
  663. # %%
  664. # ------------------------------
  665. # Model training
  666. # ------------------------------
  667. def train_elastic_net(
  668. z_expr: pd.DataFrame,
  669. y_age: pd.Series,
  670. selected_genes: List[str],
  671. n_splits: int = 5,
  672. random_state: int = 42,
  673. l1_ratios: Tuple[float, ...] = (0.1, 0.3, 0.5, 0.7, 0.9)
  674. ) -> Tuple[ElasticNetCV, dict]:
  675. """Fit ElasticNetCV on selected genes, return model and CV metrics."""
  676. # Align genes and transpose so samples are rows
  677. X = z_expr.loc[selected_genes].T
  678. y = y_age.loc[X.index] # ensure samples match
  679. # Initialize CV
  680. cv = KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
  681. # Fit ElasticNetCV on all data
  682. model = ElasticNetCV(l1_ratio=l1_ratios, alphas=None, cv=cv, max_iter=1000)
  683. model.fit(X, y)
  684. # Out-of-fold predictions
  685. preds = np.zeros(len(y), dtype=float)
  686. for train_idx, test_idx in cv.split(X):
  687. Xtr, Xte = X.iloc[train_idx], X.iloc[test_idx]
  688. ytr = y.iloc[train_idx]
  689. m = ElasticNetCV(l1_ratio=l1_ratios, alphas=None, cv=cv, max_iter=1000)
  690. m.fit(Xtr, ytr)
  691. preds[test_idx] = m.predict(Xte)
  692. # CV metrics
  693. r2 = r2_score(y, preds)
  694. mae = mean_absolute_error(y, preds)
  695. metrics = {"cv_r2": float(r2), "cv_mae": float(mae)}
  696. return model, metrics
  697. # %%
  698. # ------------------------------
  699. # Model training
  700. # ------------------------------
  701. # Features: genes x samples
  702. # Drop the 'age' column from simulated data
  703. X_sim = mvn_samples_multi_age.drop(columns=['age'])
  704. y_sim = mvn_samples_multi_age['age']
  705. # Features: genes x samples
  706. X_sim_selected = X_sim[genes] # genes are currently columns
  707. X_sim_selected = X_sim_selected.T # now genes are rows, samples are columns
  708. # Train the model
  709. model, metrics = train_elastic_net(
  710. z_expr=X_sim_selected, # or X_sim if using all genes
  711. y_age=y_sim,
  712. selected_genes=genes, # can be None to use all genes
  713. n_splits=5,
  714. random_state=42,
  715. l1_ratios=(0.1, 0.3, 0.5, 0.7, 0.9)
  716. )
  717. # %%
  718. # ------------------------------
  719. # Plotting metrics
  720. # ------------------------------
  721. # Convert to lists for plotting
  722. names = list(metrics.keys())
  723. values = list(metrics.values())
  724. # Create bar plot
  725. plt.figure(figsize=(5, 4))
  726. plt.bar(names, values, color=['skyblue', 'salmon'])
  727. plt.ylabel("Value")
  728. plt.title("ElasticNet CV Metrics")
  729. plt.ylim(0, max(values)*1.2) # add some space on top
  730. plt.show()
  731. # %%
  732. # ------------------------------
  733. # Saving Coefficients
  734. # ------------------------------
  735. # Coefficients as Series (gene names as index)
  736. coef_series = pd.Series(model.coef_, index=genes, name='coef')
  737. coef_series.to_csv("C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/07stochasticclock-08172025/00elasticnet-08152025/01elastic_net_coefficients-08152025.csv", header=True)
  738. # Optional only non-zero coeficients
  739. nonzero_coef = coef_series[coef_series != 0]
  740. nonzero_coef.to_csv("C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/07stochasticclock-08172025/00elasticnet-08152025/02elastic_net_nonzero_coefficients-08152025.csv", header=True)
  741. # Print intercept
  742. print("Intercept:", model.intercept_)
  743. # Save intercept
  744. pd.Series({"intercept": model.intercept_}).to_csv(
  745. "C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/07stochasticclock-08172025/00elasticnet-08152025/03elastic_net_intercept-08152025.csv"
  746. )
  747. # %%
  748. # ------------------------------
  749. # Predicting with Coefficients
  750. # ------------------------------
  751. # Expression matrix for excluded samples
  752. expr_excluded = expr_z
  753. # Make sure the same genes are used (selected_genes)
  754. X_excluded = expr_excluded.loc[genes]
  755. # Load coefficients
  756. df_coef = pd.read_csv("C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/07stochasticclock-08172025/00elasticnet-08152025/02elastic_net_nonzero_coefficients-08152025.csv", index_col=0)
  757. # Convert the single-column DataFrame to a Series
  758. coef_series = pd.Series(df_coef['coef'].values, index=df_coef.index, name='coef')
  759. # Ensure it's a Series
  760. coef_series = pd.Series(coef_series, name='coef')
  761. # Subset new expression matrix to selected genes
  762. X_new = X_excluded.loc[coef_series.index].T # samples x genes
  763. # Load the intercept CSV
  764. df_intercept = pd.read_csv(
  765. "C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/07stochasticclock-08172025/00elasticnet-08152025/03elastic_net_intercept-08152025.csv",
  766. index_col=0
  767. )
  768. # Extract the value
  769. intercept = df_intercept.loc["intercept"].values[0]
  770. # Dot product to get predicted age
  771. predicted_age = X_new.dot(coef_series) + intercept
  772. # Convert to Series
  773. predicted_age = pd.Series(predicted_age, index=X_new.index, name='Predicted_Age')
  774. # %%
  775. # If predicted_age is a Pandas Series
  776. plt.figure(figsize=(6,4))
  777. plt.hist(predicted_age, bins=30, edgecolor="black")
  778. plt.xlabel("Predicted Age")
  779. plt.ylabel("Frequency")
  780. plt.title("Histogram of Predicted Age")
  781. plt.show()
  782. # %%
  783. # Now align with predicted_age
  784. common_ids = predicted_age.index.intersection(meta_scaled.index)
  785. ages = meta_scaled.loc[common_ids, "AgeDeath"]
  786. pred = predicted_age.loc[common_ids]
  787. # Correlation
  788. corr = pred.corr(ages)
  789. print(f"Correlation between predicted and AgeDeath: {corr:.3f}")
  790. # Scatter plot
  791. plt.figure(figsize=(5,5))
  792. plt.scatter(ages, pred, alpha=0.6, edgecolor="k")
  793. plt.xlabel("Age at Death")
  794. plt.ylabel("Predicted Age")
  795. plt.title(f"Predicted vs Actual Age at Death\nCorrelation = {corr:.3f}")
  796. plt.show()
  797. # %%
  798. # ------------------------------
  799. # Training Deep Learning Model
  800. # ------------------------------
  801. seed = 42
  802. os.environ['PYTHONHASHSEED'] = str(seed)
  803. os.environ['TF_DETERMINISTIC_OPS'] = '1'
  804. np.random.seed(seed)
  805. random.seed(seed)
  806. tf.random.set_seed(seed)
  807. # Define a flexible Keras model
  808. def create_model(trial, input_dim):
  809. n_layers = trial.suggest_int("n_layers", 1, 4)
  810. model = Sequential()
  811. for i in range(n_layers):
  812. units = trial.suggest_int(f"units_l{i}", 32, 512, step=32)
  813. dropout_rate = trial.suggest_float(f"dropout_l{i}", 0.0, 0.5)
  814. if i == 0:
  815. model.add(Dense(
  816. units, activation='relu', input_dim=input_dim,
  817. kernel_initializer=GlorotUniform(seed=42)
  818. ))
  819. else:
  820. model.add(Dense(
  821. units, activation='relu',
  822. kernel_initializer=GlorotUniform(seed=42)
  823. ))
  824. # Fix dropout randomness
  825. model.add(Dropout(dropout_rate, seed=42))
  826. # Output layer
  827. model.add(Dense(1, activation='linear', kernel_initializer=GlorotUniform(seed=42)))
  828. lr = trial.suggest_float("learning_rate", 1e-4, 1e-2, log=True)
  829. optimizer = Adam(learning_rate=lr)
  830. model.compile(optimizer=optimizer, loss='mse', metrics=['mae'])
  831. return model
  832. # %%
  833. # ------------------------------
  834. # Training deep models from the Elastic Net with cross-validation
  835. # ------------------------------
  836. # Setting seeds for replicability
  837. seed = 42
  838. np.random.seed(seed)
  839. # Adjust shape of samples
  840. X_sim_selected = X_sim[genes] # genes are currently columns
  841. # Specify folder to save models
  842. # change this to your desired path
  843. MODEL_SAVE_PATH = "C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/07stochasticclock-08172025/01deeplearning-08162025/"
  844. os.makedirs(MODEL_SAVE_PATH, exist_ok=True)
  845. # Define a function
  846. def objective(trial):
  847. # Create model with trial hyperparameters
  848. model = create_model(trial, input_dim=X_sim_selected.shape[1])
  849. # Early stopping
  850. es = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
  851. # Train/validation split
  852. X_train, X_val, y_train, y_val = train_test_split(X_sim_selected, y_sim, test_size=0.2, random_state=42)
  853. # Train model
  854. history = model.fit(
  855. X_train, y_train,
  856. validation_data=(X_val, y_val),
  857. epochs=100,
  858. batch_size=trial.suggest_categorical("batch_size", [16, 32, 64, 128]),
  859. callbacks=[es],
  860. verbose=0
  861. )
  862. # Save the trained model in the specified folder
  863. model_filename = os.path.join(MODEL_SAVE_PATH, f"trial_{trial.number}_model.h5")
  864. model.save(model_filename)
  865. print(f"Saved model for trial {trial.number} as {model_filename}")
  866. # Return validation metric for optimization
  867. val_mae = min(history.history['val_mae'])
  868. return val_mae
  869. # Run Optuna optimization
  870. study = optuna.create_study(direction="minimize", sampler=optuna.samplers.TPESampler(seed=42))
  871. study.optimize(objective, n_trials=50)
  872. # Print best trial
  873. print("Best trial:")
  874. trial = study.best_trial
  875. print(trial.params)
  876. # %%
  877. # Build path to best model
  878. best_model_filename = os.path.join(MODEL_SAVE_PATH, f"trial_{study.best_trial.number}_model.h5")
  879. print(best_model_filename)
  880. # Print best trial
  881. print("Best trial:")
  882. trial = study.best_trial
  883. print(trial.params)
  884. # %%
  885. # ------------------------------
  886. # Prepare data for prediction
  887. # ------------------------------
  888. # Filter expr_excluded to only these genes
  889. expr_excluded_filtered = expr_z[excluded_cols].loc[genes.intersection(expr_z.index)]
  890. # Transpose to samples x genes
  891. X_excluded = expr_excluded_filtered.T
  892. X_excluded.shape
  893. # %%
  894. # ------------------------------
  895. # Load the best model (without compiling)
  896. # ------------------------------
  897. best_model_filename = "C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/07stochasticclock-08172025/01deeplearning-08162025/trial_47_model.h5"
  898. best_model = load_model(best_model_filename, compile=False)
  899. print(f"Loaded best model from {best_model_filename}")
  900. # ------------------------------
  901. # Re-compile the model with standard loss/metrics
  902. # ------------------------------
  903. best_model.compile(optimizer='adam', loss='mse', metrics=['mae'])
  904. # ------------------------------
  905. # Save under a new name
  906. # ------------------------------
  907. new_model_filename = "C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/00Stochasticdeepclock-08192025.h5"
  908. best_model.save(new_model_filename)
  909. print(f"Saved model as {new_model_filename}")
  910. # ------------------------------
  911. # Predict on new data
  912. # ------------------------------
  913. # X_excluded should have the same shape/features as the training data
  914. predicted_values = best_model.predict(X_excluded)
  915. print("Predictions shape:", predicted_values.shape)
  916. # ------------------------------
  917. # Predict
  918. # ------------------------------
  919. predicted_values = best_model.predict(X_excluded)
  920. print(predicted_values.shape)
  921. # %%
  922. # If predicted_age is a Pandas Series
  923. plt.figure(figsize=(6,4))
  924. plt.hist(predicted_values, bins=30, edgecolor="black")
  925. plt.xlabel("Predicted Age")
  926. plt.ylabel("Frequency")
  927. plt.title("Histogram of Predicted Age")
  928. plt.show()
  929. # %%
  930. # Assume X_excluded has samples as rows and their index contains the sample IDs
  931. pred = pd.Series(predicted_values.flatten(), index=X_excluded.index, name="PredictedAge")
  932. # Align with actual ages
  933. common_ids = pred.index.intersection(meta_scaled.index)
  934. ages = meta_scaled.loc[common_ids, "AgeDeath"]
  935. pred = pred.loc[common_ids]
  936. # Correlation
  937. corr = pred.corr(ages)
  938. print(f"Correlation between predicted and AgeDeath: {corr:.3f}")
  939. plt.figure(figsize=(5,5))
  940. plt.scatter(ages, pred, alpha=0.6, edgecolor="k")
  941. plt.xlabel("Age at Death")
  942. plt.ylabel("Predicted Age")
  943. plt.title(f"Predicted vs Actual Age at Death\nCorrelation = {corr:.3f}")
  944. plt.show()
  945. # %%
  946. # Saving predictions
  947. # Create a DataFrame with sample IDs and predicted age
  948. common_index = predicted_age.index.intersection(pred.index)
  949. df_pred = pd.DataFrame({
  950. "SampleID": common_index,
  951. "Predicted_Age_stochastic_elastic": predicted_age.loc[common_index].values,
  952. "Predicted_Age_stochastic_deep": pred.loc[common_index].values
  953. })
  954. # Save to CSV
  955. df_pred.to_csv(
  956. "C:/Users/jjm262/OneDrive - Yale University/Documents/Documents/00yale/04fourthyear/01projects/03tclock/02results/07stochasticclock-08172025/04vabb_prediction-08152025.csv",
  957. index=False
  958. )
  959. print("Predicted ages saved to 'predicted_age_samples.csv'.")

0stochastic-0817025.ipynb at commit a4dc4b1, no license · at the source

Overview

Authors: José J Martínez-Magaña1,2, Anna HC Vlot3, Kyle A Sullivan3, Daniel A Jacobson3, John H Krystal1,2,4, Matthew J Girgenti1,2, Diana L Núnez-Ríos1,2, Sheila T Nagamatsu1,2, Diego E Andrade-Brito1,2, Jean Merlet3,5, Alice Townsend3,5, Alana Wells6, Christiane Alvarez5, Matthew Lane5,6, Paul E Holtzheimer7, Traumatic Stress Brain Research Group, Consuelo Walss-Bass8,9, Janitza L Montalvo-Ortiz1,2,3
  1. Division of Human Genetics, Department of Psychiatry, Yale University School of Medicine, New Haven, CT, USA
  2. National Center for PTSD, US Department of Veterans Affairs, West Haven, CT, USA
  3. Biosciences Division, Oak Ridge National Laboratory, Oak Ridge, TN, USA
  4. Psychiatry Service, VA Connecticut Health Care System, West Haven, CT, USA
  5. The University of Tennessee, Knoxville, TN, USA
  6. Rhodes College, Memphis, TN, USA
  7. Department of Psychiatry, Geisel School of Medicine at Dartmouth, Lebanon, NH 03756, USA
  8. Louis A. Faillace, MD, Department of Psychiatry and Behavioral Sciences, McGovern Medical School, University of Texas Health Science Center at Houston, Houston, TX, USA
  9. MD Anderson Cancer Center, University of Texas Health Science Center at Houston Graduate School of Biomedical Sciences, Houston, TX, USA
Journal: iScience, volume 29, issue 7, article 116439
Dates: received 1 May 2024; accepted 2 June 2026; published online 2 July 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1016/j.isci.2026.116439 · PMID 42491807 · PMCID PMC13378135 · OpenAlex W4366998610
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), human (organism)
Methods: Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, Connectivity
Keywords: Biological sciences, Molecular biology, Neuroscience, Molecular neuroscience, Bioinformatics, Computational bioinformatics, Genomic analysis
Topic: Bioinformatics and Genomic Networks (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: CSRD VA (IK2 CX002095); National Institute of Mental Health (MH120170); National Institute on Drug Abuse (DP1DA058737, R21DA050160); National Center for PTSD, U.S. Department of Veterans Affairs; NIDA NIH HHS (R21 DA050160, DP1 DA058737); Office of Science; U.S. Department of Energy (DE-AC05-00OR22725); US Department of Veterans Affairs (1IK2CX002095-01A1); NIMH NIH HHS (R01 MH120170)
Citations: cited by 1 paper (Europe PMC); 125 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repositories

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

martinezjaime/PFC-Transcriptomic

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: a4dc4b14a9abe71a0592da6aeb67085ad3e86026, 20 April 2026
Languages: Jupyter (26), R (11), Shell (6)
Size: 51 files, 43 scripts
Software Heritage: not archived
Found in: the resources table
Holds: README, documentation, 26 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration
Tools: pandas (23 files), NumPy (22 files), scikit-learn (20 files), Matplotlib (17 files), SciPy (17 files), seaborn (14 files), statsmodels (7 files), data.table (6 files), tidyverse (6 files), glmnet (4 files), Keras (4 files), TensorFlow (4 files), NetworkX (2 files), ggplot2 (1 file), patchwork (1 file), STAR (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
44 files

Jacobson-CompSysBio/MENTOR-py

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 9bea8a4963bccf5d5842c72dcc6f6207b164a752, 17 July 2024
Languages: Python (14), R (8)
Size: 32 files, 22 scripts
Software Heritage: not archived
Found in: “Data and code availability”
Holds: README, license file, CITATION.cff, environment (environment.yml, pyproject.toml, setup.cfg), tests
Not found: continuous integration, documentation
Tools: NumPy (12 files), pandas (9 files), scikit-learn (8 files), SciPy (8 files), tidyverse (7 files), data.table (2 files), igraph (2 files), circlize (1 file), ComplexHeatmap (1 file), cowplot (1 file), patchwork (1 file), reshape2 (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
24 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;
  • 65 scripts, each with its path and the digest of its content;
  • 24 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

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1016/j.isci.2026.116439.

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

  • Authors: added José J Martínez-Magaña (0000-0003-0390-8252); Janitza L Montalvo-Ortiz (0000-0002-7657-8365); removed José J Martínez-Magaña; Janitza L Montalvo-Ortiz

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 18 authors, 7 keywords, 9 funders, 125 references.

Cite

This paper

Martínez-Magaña, J. J., Vlot, A. H., Sullivan, K. A., Jacobson, D. A., Krystal, J. H., Girgenti, M. J., Núnez-Ríos, D. L., Nagamatsu, S. T., Andrade-Brito, D. E., Merlet, J., Townsend, A., Wells, A., Alvarez, C., Lane, M., Holtzheimer, P. E., Traumatic Stress Brain Research Group, Walss-Bass, C., & Montalvo-Ortiz, J. L. (2026). Decoding the role of transcriptomic clocks in the human prefrontal cortex. iScience, 29(7), 116439. https://doi.org/10.1016/j.isci.2026.116439

BibTeX

@article{martinezmagana2026decoding,
author = {Martínez-Magaña, José J and Vlot, Anna HC and Sullivan, Kyle A and Jacobson, Daniel A and Krystal, John H and Girgenti, Matthew J and Núnez-Ríos, Diana L and Nagamatsu, Sheila T and Andrade-Brito, Diego E and Merlet, Jean and Townsend, Alice and Wells, Alana and Alvarez, Christiane and Lane, Matthew and Holtzheimer, Paul E and {Traumatic Stress Brain Research Group} and Walss-Bass, Consuelo and Montalvo-Ortiz, Janitza L},
title = {{Decoding the role of transcriptomic clocks in the human prefrontal cortex}},
journal = {iScience},
year = {2026},
month = jul,
volume = {29},
number = {7},
pages = {116439},
publisher = {Elsevier},
issn = {2589-0042},
doi = {10.1016/j.isci.2026.116439},
url = {https://doi.org/10.1016/j.isci.2026.116439},
pmid = {42491807},
pmcid = {PMC13378135}
}

RIS

TY - JOUR
AU - Martínez-Magaña, José J
AU - Vlot, Anna HC
AU - Sullivan, Kyle A
AU - Jacobson, Daniel A
AU - Krystal, John H
AU - Girgenti, Matthew J
AU - Núnez-Ríos, Diana L
AU - Nagamatsu, Sheila T
AU - Andrade-Brito, Diego E
AU - Merlet, Jean
AU - Townsend, Alice
AU - Wells, Alana
AU - Alvarez, Christiane
AU - Lane, Matthew
AU - Holtzheimer, Paul E
AU - Traumatic Stress Brain Research Group
AU - Walss-Bass, Consuelo
AU - Montalvo-Ortiz, Janitza L
TI - Decoding the role of transcriptomic clocks in the human prefrontal cortex
T2 - iScience
J2 - iScience
PY - 2026
DA - 2026/07/02
VL - 29
IS - 7
SP - 116439
SN - 2589-0042
PB - Elsevier
DO - 10.1016/j.isci.2026.116439
UR - https://doi.org/10.1016/j.isci.2026.116439
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.isci.2026.116439",
"type": "article-journal",
"title": "Decoding the role of transcriptomic clocks in the human prefrontal cortex",
"container-title": "iScience",
"author": [
{
"family": "Martínez-Magaña",
"given": "José J"
},
{
"family": "Vlot",
"given": "Anna HC"
},
{
"family": "Sullivan",
"given": "Kyle A"
},
{
"family": "Jacobson",
"given": "Daniel A"
},
{
"family": "Krystal",
"given": "John H"
},
{
"family": "Girgenti",
"given": "Matthew J"
},
{
"family": "Núnez-Ríos",
"given": "Diana L"
},
{
"family": "Nagamatsu",
"given": "Sheila T"
},
{
"family": "Andrade-Brito",
"given": "Diego E"
},
{
"family": "Merlet",
"given": "Jean"
},
{
"family": "Townsend",
"given": "Alice"
},
{
"family": "Wells",
"given": "Alana"
},
{
"family": "Alvarez",
"given": "Christiane"
},
{
"family": "Lane",
"given": "Matthew"
},
{
"family": "Holtzheimer",
"given": "Paul E"
},
{
"literal": "Traumatic Stress Brain Research Group"
},
{
"family": "Walss-Bass",
"given": "Consuelo"
},
{
"family": "Montalvo-Ortiz",
"given": "Janitza L"
}
],
"container-title-short": "iScience",
"volume": "29",
"issue": "7",
"page": "116439",
"DOI": "10.1016/j.isci.2026.116439",
"PMID": "42491807",
"PMCID": "PMC13378135",
"ISSN": "2589-0042",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.isci.2026.116439",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
2
]
]
}
}

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/s41514-026-00391-9 [code]
Region-specific transcriptional signatures of brain aging in the absence of neuropathology at the single-cell level.
Journal: npj aging
In common: circlize, ComplexHeatmap, cowplot, 12 other tools, genetics / omics, 5 references
[2] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: STAR, igraph, circlize, 14 other tools, genetics / omics, 3 references
[3] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: glmnet, Keras, igraph, 16 other tools
[4] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: Keras, igraph, circlize, 15 other tools, genetics / omics, 1 reference
[5] doi:10.1038/s41598-026-48613-0 [code]
An snRNA-seq aging clock for the fruit fly head sheds light on sex-biased aging.
Journal: Scientific reports
In common: Keras, TensorFlow, seaborn, 5 other tools, genetics / omics, 8 references
[6] doi:10.1126/sciadv.aeg3223 [code]
The extreme diversity of retinal amacrine cells has deep evolutionary roots.
Journal: Science advances
In common: glmnet, igraph, circlize, 13 other tools, genetics / omics, 1 reference
[7] doi:10.1093/neuonc/noag128 [code]
Spatially-resolved single-cell imaging of melanoma brain metastases identifies localized immune patterns predictive of immune checkpoint blockade response.
Journal: Neuro-oncology
In common: glmnet, Keras, circlize, 12 other tools, genetics / omics
[8] doi:10.1038/s41586-026-10629-x [code]
Whole-genome duplication shaped cell-type evolution in the vertebrate brain.
Journal: Nature
In common: igraph, circlize, NetworkX, 12 other tools, genetics / omics, 2 references
[9] doi:10.1038/s41467-026-76676-0 [code]
Determinants of functional burden pleiotropy and gene dosage responses across human traits.
Journal: Nature communications
In common: igraph, circlize, ComplexHeatmap, 13 other tools, genetics / omics
[10] doi:10.1038/s41467-026-76675-1 [code]
Long-read proteogenomic atlas of human neuronal differentiation reveals isoform diversity informing neurodevelopmental risk mechanisms.
Journal: Nature communications
In common: STAR, igraph, NetworkX, 12 other tools, genetics / omics, 1 reference

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.