OSCR

Subgingival microbiota composition is associated with brain health in the general population-the PAROMIND study.

Code ↔ Paper

4 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 4 matches
  1. [1] § Methods › Topological data analysis framework › SAFE: enrichment analysis ↔ analysis.ipynb, lines 29–92 · score 0.99 · Geriatric Depression Scale, diastolic blood pressure, Mini Mental State, blood triglycerides, MEDAS score, MIND score
  2. [2] § Methods › Phenotyping › Cognitive and mental health assessments ↔ analysis.ipynb, lines 29–92 · score 0.82 · Geriatric Depression Scale, Mini Mental State, Recall, Vocabulary, Word, inverted
  3. [3] § Methods › Topological data analysis framework › SAFE: enrichment analysis ↔ analysis.ipynb, lines 596–662 · score 0.81 · robust Aitchison distance, forward model selection, ordiR2step, matrix, vegan, variance
  4. [4] § Methods › Group analysis ↔ analysis.ipynb, lines 1533–1548 · score 0.75 · diastolic blood pressure, BMI, HDL, HbA1c, cholesterol, triglycerides

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 · 2,057 lines · 68 KB · no license · 4 matches

  1. # %% [markdown]
  2. # # Import libraries and data
  3. # %%
  4. import numpy as np
  5. np.int = np.int32
  6. from sklearn.preprocessing import MinMaxScaler
  7. from sklearn.cluster import DBSCAN
  8. from tmap.tda import mapper, Filter
  9. from tmap.tda.cover import Cover
  10. from tmap.tda.metric import Metric
  11. from tmap.tda.utils import optimize_dbscan_eps
  12. import pandas as pd
  13. import networkx as nx
  14. # %%
  15. from pathlib import Path
  16. code_dir=Path.cwd()
  17. project_dir=code_dir.parent
  18. input_dir=project_dir/"input"
  19. output_dir=project_dir/"output/tda/"
  20. tmp_dir=project_dir/"tmp"
  21. output_dir.mkdir(exist_ok=True, parents=True)
  22. # %% [markdown]
  23. # # Prepare data
  24. #
  25. # %%
  26. # Grouping variables in categories
  27. demographics = [
  28. "demographics_age",
  29. "demographics_sex",
  30. "demographics_education_isced",
  31. ]
  32. cognitive_scores = [
  33. "cognition_g_factor_inverted",
  34. "cognition_tmt_a_inverted",
  35. "cognition_tmt_b_inverted",
  36. 'cognition_vocabulary_b_test',
  37. 'cognition_word_list_recall',
  38. 'cognition_animal_naming_test',
  39. 'cognition_mini_mental_state_exam',
  40. ]
  41. neuropsychiatric_scores = [
  42. 'psych_phq9_sum',
  43. 'psych_phq15_sum',
  44. 'psych_geriatrics_depression_scale',
  45. 'psych_gad7_sum',
  46. ]
  47. cardiovascular_risk_factors = [
  48. 'cvrisk_systolic_blood_pressure_mmhg',
  49. 'cvrisk_diastolic_blood_pressure_mmhg',
  50. 'cvrisk_BMI',
  51. 'cvrisk_smoking_currently',
  52. "blood_cholesterol_mg_dl",
  53. "blood_hdl_mg_dl",
  54. "blood_ldl_mg_dl",
  55. "blood_triglycerides_mg_dl",
  56. "blood_hba1c",
  57. ]
  58. inflammation = [
  59. "inflammation_hsCRP",
  60. "inflammation_leukocytes",
  61. ]
  62. paro_variables = [
  63. "paro_cal_mean",
  64. "paro_dmft",
  65. "paro_plaqueindex",
  66. "paro_bop"
  67. ]
  68. diet_scores = [
  69. "diet_mind_score",
  70. "diet_medas_score",
  71. "diet_dash1_score"
  72. ]
  73. microbiome_variables = oral_microbiome_genus.columns.tolist()
  74. imaging_means = ['imaging_thickness_volume_mean','imaging_fw_gm_mean', 'imaging_fw_wm_mean', 'imaging_fat_gm_mean', 'imaging_fat_wm_mean']
  75. metadata_variables = demographics + cognitive_scores + neuropsychiatric_scores + cardiovascular_risk_factors + inflammation + paro_variables + imaging_means + diet_scores
  76. # %%
  77. # Setup naming dictionary (shortened for brevity)
  78. variable_styling_dict = {
  79. "base_age":"Age",
  80. "...":"...",
  81. }
  82. # %%
  83. # Load taxa abundance data, sample metadata
  84. metadata = pd.read_csv(input_dir/"data/metadata_df.csv", index_col=0)
  85. oral_microbiome_genus = pd.read_csv(input_dir/"data/oral_microbiome_genus.csv", index_col=0)
  86. X = oral_microbiome_genus
  87. metadata = metadata.loc[metadata.index.isin(X.index)][metadata_variables]
  88. X = X.loc[X.index.isin(metadata.index)]
  89. # %%
  90. # z-score non-microbiome data
  91. from sklearn.preprocessing import StandardScaler
  92. def is_binary(series):
  93. return set(series.dropna().unique()) <= {0, 1}
  94. binary_columns = [col for col in metadata.columns if is_binary(metadata[col])]
  95. metadata[binary_columns] = metadata[binary_columns].astype('object')
  96. continuous_columns = [col for col in metadata.columns if not is_binary(metadata[col])]
  97. metadata[continuous_columns] = StandardScaler().fit_transform(metadata[continuous_columns])
  98. # %%
  99. metadata_categories = [col.split("_")[0] for col in metadata.columns.tolist()]
  100. microbiome_categories = ["genus"] * len(X.columns.to_list())
  101. # %% [markdown]
  102. # # Aitchison distance
  103. # %%
  104. from biom.table import Table
  105. data = X.T.to_numpy()
  106. samples = X.index.to_list()
  107. observation = X.columns.to_list()
  108. table = Table(data, observation, samples)
  109. from gemelli.rpca import rpca
  110. _, distance = rpca(table)
  111. dm = distance.to_data_frame()
  112. # %% [markdown]
  113. # # Mapper
  114. # %%
  115. # Initiate a Mapper object
  116. tm = mapper.Mapper(verbose=1)
  117. # Projection
  118. metric = Metric(metric="precomputed")
  119. lens = [Filter.PCOA(components=[0, 1], metric=metric, random_state=100)]
  120. projected_X = tm.filter(dm, lens=lens)
  121. # Covering, clustering & mapping
  122. eps = optimize_dbscan_eps(X, threshold=95)
  123. clusterer = DBSCAN(eps=eps, min_samples=3)
  124. cover = Cover(projected_data=MinMaxScaler().fit_transform(projected_X), resolution=30, overlap=1.5)
  125. graph = tm.map(data=X, cover=cover, clusterer=clusterer)
  126. print(graph.info())
  127. # %%
  128. # Apply spring embedding
  129. initial_nodepos = {idx:graph.nodePos[idx] for idx in range(graph.nodePos.shape[0])}
  130. pos = nx.spring_layout(graph, k = 0.2, pos = initial_nodepos, seed=42)
  131. graph.nodePos = np.array([pos[key] for key in pos.keys()])
  132. for idx, node in enumerate(graph.nodes):
  133. graph.nodes[idx]["pos"] = pos[idx].tolist()
  134. import pickle
  135. pickle.dump(graph, open(output_dir/"graph.pickle", "wb"))
  136. edgelist_3col = nx.to_pandas_edgelist(graph)
  137. edgelist_3col["dist"] = 1
  138. edgelist_3col.to_csv(output_dir/"mapper_graph_3col.txt", sep="\t", index=False, header=None)
  139. # %% [markdown]
  140. # # SAFE
  141. # %%
  142. # Setup function to transform data from subject to node level
  143. def transform2node_data(graph, data, mode='mean'):
  144. map_fun = {'sum': np.sum,
  145. "mean": np.nanmean}
  146. if mode not in ["sum", "mean"]:
  147. raise SyntaxError('Wrong provided parameters.')
  148. else:
  149. aggregated_fun = map_fun[mode]
  150. nodes = graph.nodes
  151. dv = data.values
  152. if data is not None:
  153. node_data = {nid: aggregated_fun(dv[attr['sample'], :], 0)
  154. for nid, attr in nodes.items()}
  155. node_data = pd.DataFrame.from_dict(node_data,
  156. orient='index',
  157. columns=data.columns)
  158. return node_data
  159. # %%
  160. node_subject_mapping = {node:list(graph.nodes[idx]["sample_names"]) for idx,node in enumerate(graph.nodes)}
  161. def transform2node_data_bin(df, node_subject_mapping):
  162. # Computes node level percentage of binary variables
  163. t = [(k, x) for k, v in node_subject_mapping.items() for x in v]
  164. hierarchical_index = pd.MultiIndex.from_tuples(t)
  165. df_long = pd.DataFrame(index=hierarchical_index, columns=df.columns)
  166. df_long.index.rename(["node","subject"], inplace=True)
  167. for node,sub in df_long.index:
  168. df_long.loc[(node,sub),:] = df.loc[sub,:]
  169. for col in df_long.columns :
  170. df_long[col] = df_long[col].astype(object)
  171. df_long.reset_index(inplace=True)
  172. df_transformed = df_long[["node"] + df.columns.tolist()].groupby("node").agg(np.sum) / df_long[["node"] + df.columns.tolist()].groupby("node").agg(len)
  173. return df_transformed
  174. # %%
  175. # Prepare data to be used by safepy
  176. metadata_transformed = transform2node_data(graph, metadata.drop(['demographics_sex', 'cvrisk_smoking_currently'], axis=1), mode="mean")
  177. binary_transformed = transform2node_data_bin(metadata_plus_imaging[['demographics_sex', 'cvrisk_smoking_currently']], node_subject_mapping)
  178. metadata_transformed = metadata_transformed.join(binary_transformed)
  179. oral_microbiome_genus_transformed = transform2node_data(graph, oral_microbiome_genus, mode="mean")
  180. data_transformed = metadata_transformed.join(oral_microbiome_genus_transformed)
  181. data_transformed.to_csv(output_dir/"mapper_graph_metadata.txt", sep="\t", index=True)
  182. # %%
  183. # Perform SAFE
  184. from safepy import safe
  185. sf = safe.SAFE(path_to_safe_data=f"{output_dir}/safe/")
  186. sf.random_seed = 0
  187. sf.load_network(network_file=f"{output_dir}/mapper_graph_3col.txt")
  188. sf.load_attributes(attribute_file=f"{output_dir}/mapper_graph_metadata.txt")
  189. sf.define_neighborhoods()
  190. num_permutations = 5000
  191. sf.compute_pvalues(num_permutations=num_permutations)
  192. # %%
  193. network_enrichment_scores = pd.DataFrame(sf.nes, columns=data_transformed.columns)
  194. network_enrichment_scores_signif = pd.DataFrame(sf.nes_binary, columns=data_transformed.columns)
  195. network_enrichment_scores_signif_pos = (network_enrichment_scores > 0) & (network_enrichment_scores_signif)
  196. network_enrichment_scores_signif_neg = (network_enrichment_scores < 0) & (network_enrichment_scores_signif)
  197. # %%
  198. nes_min = network_enrichment_scores.min().min()
  199. nes_max = network_enrichment_scores.max().max()
  200. print(nes_min, nes_max)
  201. # %%
  202. safe_summary = sf.attributes.copy()
  203. safe_summary.drop("id", axis=1)
  204. safe_summary.set_index("name", inplace=True)
  205. # %%
  206. # Compute enrichment ratio + extract positive and negative enrichment
  207. # enrichment ratio = n of enriched neighborhoods / n of nodes
  208. safe_summary["enrichment_ratio"] = safe_summary["num_neighborhoods_enriched"] / len(graph.nodes)
  209. safe_summary["num_neighborhoods_enriched_pos"] = pd.DataFrame(network_enrichment_scores_signif_pos.sum(axis=0), index = safe_summary.index)
  210. safe_summary["num_neighborhoods_enriched_neg"] = pd.DataFrame(network_enrichment_scores_signif_neg.sum(axis=0), index = safe_summary.index)
  211. safe_summary["category"] = [idx[0] for idx in safe_summary.index.str.split("_")]
  212. safe_summary.to_csv(output_dir/'metadata_safe_summary.csv')
  213. # %% [markdown]
  214. # # Draw PCA based on SAFE score
  215. # %%
  216. color_codes = {
  217. 'genus': '#68D391', # A fresh green
  218. 'cognition': '#D53F8C', # A vibrant pink
  219. 'psych': '#FFD700', # Gold
  220. 'cvrisk': '#1E90FF', # DodgerBlue
  221. 'blood': '#1E90FF', # dark red
  222. 'inflammation': '#9E2A2B', # dark red
  223. "diet": "#036c5f", #Teal
  224. 'paro': '#DD6B20', # A distinctive orange
  225. 'demographics': '#808080', # DarkSlateGray
  226. 'imaging': '#8A2BE2', # BlueViolet
  227. }
  228. color_codes_light = {
  229. 'genus': '#a3ebc7', # pastel green
  230. 'cognition': '#f086b1', # pastel pink
  231. 'psych': '#ffeb99', # pastel gold/yellow
  232. 'cvrisk': '#80c4ff', # pastel blue
  233. 'blood': '#80c4ff', # pastel blue
  234. 'inflammation': '#d16b6b', # pastel red
  235. "diet": "#4d9e8f", #pastel teal
  236. 'paro': '#ffae73', # pastel orange
  237. 'demographics': '#b3b3b3', # pastel gray
  238. 'imaging': '#c393f7' # pastel violet
  239. }
  240. # %%
  241. import plotly.offline as py_offline
  242. import plotly.graph_objs as go
  243. from sklearn.decomposition import PCA
  244. from sklearn.preprocessing import MinMaxScaler
  245. import pandas as pd
  246. # Perform PCA
  247. pca = PCA()
  248. pca_result = pca.fit_transform(network_enrichment_scores.T)
  249. # Select top enrichments for metadata and oral microbiome genus
  250. top_10_col_sorter = lambda df, col: df.sort_values(col, ascending=False).head(10).index
  251. top_metadata = top_10_col_sorter(safe_summary.loc[data_transformed.columns], 'enrichment_ratio')
  252. top_genus = top_10_col_sorter(safe_summary.loc[oral_microbiome_genus.columns], 'enrichment_ratio')
  253. # Normalize enrichment ratio
  254. mx_scale = MinMaxScaler(feature_range=(10, 30))
  255. scaled_enrichment = mx_scale.fit_transform(safe_summary[['enrichment_ratio']])
  256. # Prepare data for plotting
  257. data = []
  258. categories = safe_summary['category'].unique()
  259. for cat in categories:
  260. mask = safe_summary['category'] == cat
  261. category_data = safe_summary[mask]
  262. scaled_size = scaled_enrichment[mask].flatten()
  263. trace = go.Scatter(
  264. x=pca_result[mask, 0],
  265. y=pca_result[mask, 1],
  266. mode='markers',
  267. name=variable_styling_dict[cat],
  268. marker=dict(
  269. color=color_codes[cat],
  270. size=10,#scaled_size, #
  271. opacity=0.5
  272. ),
  273. text=[variable_styling_dict[cat] if not "genus" in cat else " ".join(cat.split("_")[1:]) for cat in category_data.index],
  274. textfont=dict(
  275. size=12 # specify the size of the text
  276. ),
  277. )
  278. data.append(trace)
  279. # Layout configuration
  280. layout = go.Layout(
  281. xaxis=dict(title=f"PC1 ({pca.explained_variance_ratio_[0] * 100:.2f}%)"),
  282. yaxis=dict(title=f"PC2 ({pca.explained_variance_ratio_[1] * 100:.2f}%)"),
  283. width=1200, height=500,
  284. title="",
  285. font=dict(size=15),
  286. hovermode='closest'
  287. )
  288. # Plot using offline mode in Plotly
  289. fig = go.Figure(data=data, layout=layout)
  290. py_offline.plot(fig, auto_open=False)
  291. fig.write_html(output_dir/"enrichment_analysis/PCA.html")
  292. fig.write_image(output_dir/"enrichment_analysis/PCA.png", format="png", scale=10)
  293. fig.write_image(output_dir/"enrichment_analysis/PCA.svg", format="svg")
  294. pyo.iplot(fig, config={'responsive': True}) # Use iframe as the renderer
  295. # %%
  296. import plotly.offline as py_offline
  297. import plotly.graph_objs as go
  298. from sklearn.decomposition import PCA
  299. from sklearn.preprocessing import MinMaxScaler
  300. import pandas as pd
  301. import numpy as np
  302. def avoid_text_overlap(x, y, text_list, offset=0.02):
  303. new_positions = []
  304. seen_positions = set()
  305. for i in range(len(x)):
  306. new_x, new_y = x[i], y[i]
  307. while (new_x, new_y) in seen_positions:
  308. new_y += offset
  309. seen_positions.add((new_x, new_y))
  310. new_positions.append((new_x, new_y))
  311. return new_positions
  312. # Perform PCA
  313. pca = PCA()
  314. pca_result = pca.fit_transform(network_enrichment_scores.T)
  315. # Select top enrichments for metadata and oral microbiome genus
  316. top_10_col_sorter = lambda df, col: df.sort_values(col, ascending=False).head(10).index
  317. top_metadata = top_10_col_sorter(safe_summary.loc[data_transformed.columns], 'enrichment_ratio')
  318. top_genus = top_10_col_sorter(safe_summary.loc[oral_microbiome_genus.columns], 'enrichment_ratio')
  319. # Normalize enrichment ratio
  320. mx_scale = MinMaxScaler(feature_range=(10, 30))
  321. scaled_enrichment = mx_scale.fit_transform(safe_summary[['enrichment_ratio']])
  322. # Prepare data for plotting
  323. data = []
  324. categories = safe_summary['category'].unique()
  325. for cat in categories:
  326. mask = safe_summary['category'] == cat
  327. category_data = safe_summary[mask]
  328. scaled_size = scaled_enrichment[mask].flatten()
  329. x_coords = pca_result[mask, 0]
  330. y_coords = pca_result[mask, 1]
  331. text_labels = [
  332. variable_styling_dict[cat] if not "genus" in cat else " ".join(cat.split("_")[1:])
  333. for cat in category_data.index
  334. ]
  335. # Adjust text positions to avoid overlap
  336. adjusted_positions = avoid_text_overlap(x_coords, y_coords, text_labels)
  337. trace = go.Scatter(
  338. x=[pos[0] for pos in adjusted_positions],
  339. y=[pos[1] for pos in adjusted_positions],
  340. mode='markers+text',
  341. name=variable_styling_dict[cat],
  342. marker=dict(
  343. color=color_codes[cat],
  344. size=10, # scaled_size,
  345. opacity=0.5
  346. ),
  347. text=text_labels,
  348. textfont=dict(
  349. size=10 # specify the size of the text
  350. ),
  351. showlegend = False
  352. )
  353. data.append(trace)
  354. # Layout configuration
  355. layout = go.Layout(
  356. xaxis=dict(title=f"PC1 ({pca.explained_variance_ratio_[0] * 100:.2f}%)"),
  357. yaxis=dict(title=f"PC2 ({pca.explained_variance_ratio_[1] * 100:.2f}%)"),
  358. width=1322*1.5, height=794*1.5,
  359. title="",
  360. font=dict(size=15),
  361. hovermode='closest',
  362. template="plotly_white"
  363. )
  364. # Plot using offline mode in Plotly
  365. fig = go.Figure(data=data, layout=layout)
  366. py_offline.plot(fig, auto_open=False)
  367. # Uncomment next line if interactive plot within a notebook is required
  368. py_offline.iplot(fig)
  369. fig.write_html(str(output_dir / "enrichment_analysis/PCA_annotated.html"))
  370. fig.write_image(str(output_dir / "enrichment_analysis/PCA_annotated.png"), format="png", scale=10)
  371. fig.write_image(str(output_dir / "enrichment_analysis/PCA_annotated.svg"), format="svg")
  372. # %% [markdown]
  373. # # envfit
  374. # %%
  375. Path(output_dir/"envfit/").mkdir(exist_ok=True)
  376. X.to_csv(output_dir/'envfit/microbiome.csv',index=True)
  377. metadata.astype(float).to_csv(output_dir/'envfit/metadata.csv',index=True)
  378. # %%
  379. import rpy2.robjects as robjects
  380. from rpy2.robjects.packages import importr
  381. import numpy as np
  382. from statsmodels.sandbox.stats.multicomp import multipletests
  383. importr("vegan")
  384. def envfit_metadata(genus_path,metadata_path):
  385. rcode = """
  386. genus_table <- read.csv('{path_data}',row.names = 1)
  387. metadata <- read.csv('{path_metadata}',row.names = 1,check.names=FALSE)
  388. dist <- vegdist(genus_table, method="robust.aitchison")
  389. ord <- capscale(dist ~ -1)
  390. """.format(path_data=genus_path,path_metadata=metadata_path)
  391. robjects.r(rcode)
  392. envfit_result = robjects.r(
  393. """
  394. fit <- envfit(ord,metadata,permutations = 5000, na.rm=TRUE)
  395. fit$vectors
  396. """)
  397. fit_result = pd.DataFrame(columns=["r2","pvals","Source","End"],index=envfit_result[envfit_result.names.index("arrows")].rownames)
  398. fit_result.loc[:,"r2"] = envfit_result[envfit_result.names.index("r")]
  399. fit_result.loc[:, "pvals"] = envfit_result[envfit_result.names.index("pvals")]
  400. fit_result.loc[:, ["Source","End"]] = np.array(envfit_result[envfit_result.names.index("arrows")])
  401. return fit_result
  402. import time
  403. t1 = time.time()
  404. envfit_df = envfit_metadata(f'{output_dir}/envfit/microbiome.csv',
  405. f'{output_dir}/envfit/metadata.csv',
  406. )
  407. envfit_df["pvals_fdr"] = multipletests(envfit_df["pvals"], method='fdr_bh')[1]
  408. print('envfit takes', time.time() - t1)
  409. # %%
  410. n_envfit = X.shape[0]
  411. p_envfit = 1
  412. envfit_df["adjusted_r2"] = 1 - (1 - envfit_df["r2"]) * ((n_envfit - 1) / (n_envfit - p_envfit -1))
  413. # %% [markdown]
  414. # # adonis
  415. # %%
  416. Path(output_dir/"adonis/").mkdir(exist_ok=True)
  417. X.to_csv(output_dir/'adonis/microbiome.csv',index=True)
  418. # %%
  419. # Perform PERMANOVA with adonis
  420. import rpy2.robjects.pandas2ri as rpypandas
  421. import rpy2.robjects as robjects
  422. from rpy2.robjects.packages import importr
  423. importr("vegan")
  424. def run_adonis(genus_path, metadata_path):
  425. """
  426. Perform cumulative PERMANOVA using adonis to assess the variance explained by all metadata covariates.
  427. Returns a pandas DataFrame with adonis results.
  428. """
  429. r_code = f"""
  430. genus_table <- read.csv('{genus_path}', row.names = 1)
  431. metadata <- read.csv('{metadata_path}', row.names = 1, check.names = FALSE)
  432. dist <- vegdist(genus_table, method = "robust.aitchison")
  433. adonis_result <- adonis2(dist ~ ., data = metadata, na.action = na.omit, permutations = 5000,
  434. by = "margin"
  435. )
  436. list(adonis_table = adonis_result)
  437. """
  438. r_output = robjects.r(r_code)
  439. adonis_table_r = r_output.rx2("adonis_table")
  440. adonis_results_df = rpypandas.rpy2py_dataframe(adonis_table_r)
  441. return adonis_results_df
  442. adonis_df = pd.DataFrame(columns=["r2","pvals"])
  443. for col in metadata.columns:
  444. metadata[col].to_csv(output_dir/'adonis/metadata.csv',index=True)
  445. fit_result = run_adonis(f'{output_dir}/adonis/microbiome.csv', f'{output_dir}/adonis/metadata.csv')
  446. adonis_df.loc[col,"r2"] = fit_result.loc[col,"R2"]
  447. adonis_df.loc[col,"pvals"] = fit_result.loc[col,"Pr(>F)"]
  448. adonis_df.loc["pvals_fdr"] = multipletests(adonis_df["pvals"], method='fdr_bh')[1]
  449. adonis_df.to_csv(output_dir/'adonis/adonis_results.csv', index=True)
  450. # %%
  451. n_adonis = X.shape[0]
  452. p_adonis = 1
  453. adonis_df["adjusted_r2"] = 1 - (1 - adonis_df["r2"]) * ((n_adonis - 1) / (n_adonis - p_adonis -1))
  454. # %%
  455. # Merge enrichment ratio, envfit and adonis results into a single table
  456. compared_table = safe_summary.loc[metadata.columns].sort_values('enrichment_ratio',ascending=False)
  457. neg_log_p_thresh = -np.log10(0.05)
  458. compared_table.loc[:,'envfit_adjusted_r2'] = list(envfit_df.loc[compared_table.index,'adjusted_r2'])
  459. compared_table.loc[:,'envfit_p_fdr'] = list(envfit_df.loc[compared_table.index,'pvals_fdr'])
  460. compared_table.loc[:,'envfit_neg_log_p_fdr'] = -np.log10(compared_table.loc[:,'envfit_p_fdr'])
  461. compared_table.loc[:, 'envfit_signif'] = compared_table['envfit_p_fdr'] < 0.05
  462. compared_table.loc[:,'adonis_adjusted_r2'] = list(adonis_df.loc[compared_table.index,'adjusted_r2'])
  463. compared_table.loc[:,'adonis_p_fdr'] = list(adonis_df.loc[compared_table.index,'pvals_fdr'])
  464. compared_table.loc[:,'adonis_neg_log_p_fdr'] = -np.log10(compared_table.loc[:,'adonis_p_fdr'])
  465. compared_table.loc[:, 'adonis_signif'] = compared_table['adonis_p_fdr'] < 0.05
  466. compared_table = compared_table.fillna(0)
  467. compared_table.index = [variable_styling_dict[var] for var in compared_table.index]
  468. compared_table.to_csv(output_dir/'enrichment_analysis/comparison_table.csv',index=True)
  469. # %%
  470. from scipy.stats import spearmanr
  471. print(spearmanr(compared_table["enrichment_ratio"], compared_table["envfit_adjusted_r2"]))
  472. print(spearmanr(compared_table["enrichment_ratio"], compared_table["adonis_adjusted_r2"]))
  473. # %% [markdown]
  474. # # ordiR2step
  475. # %%
  476. import pandas as pd
  477. import rpy2.robjects.pandas2ri as rpypandas
  478. import rpy2.robjects as robjects
  479. from rpy2.robjects.packages import importr
  480. from pathlib import Path
  481. # Ensure the R package 'vegan' is imported
  482. importr("vegan")
  483. def run_ordiR2step(genus_path, metadata_path, output_path):
  484. """
  485. Performs forward model selection using ordiR2step to find the best set of predictors.
  486. This function calculates the robust Aitchison distance and then uses ordiR2step
  487. to build a cumulative model, adding variables one by one based on their
  488. contribution to the explained variance (R-squared).
  489. The final table of the best model is saved to a CSV file.
  490. Args:
  491. genus_path (str): Path to the microbiome genus data CSV file.
  492. metadata_path (str): Path to the full metadata CSV file with all potential predictors.
  493. output_path (str): Path to save the resulting ANOVA table CSV.
  494. """
  495. print("Performing forward model selection with ordiR2step...")
  496. # R code to be executed
  497. r_code = """
  498. # 1. Load data
  499. genus_table <- read.csv('{genus_path}', row.names = 1)
  500. metadata <- read.csv('{metadata_path}', row.names = 1, check.names=FALSE)
  501. # 2. Calculate the distance matrix
  502. dist <- vegdist(genus_table, method="robust.aitchison")
  503. # 3. Define the null and full models for the selection scope
  504. # Null model (starting point)
  505. mod0 <- capscale(dist ~ 1, data=metadata, na.action=na.omit)
  506. # Full model
  507. mod1 <- capscale(dist ~ ., data=metadata, na.action=na.omit)
  508. # 4. Perform the forward selection using ordiR2step
  509. step_result <- ordiR2step(mod0, scope = formula(mod1), direction = "forward", permutations = 5000)
  510. # 5. Extract the ANOVA table from the final selected model
  511. final_anova <- as.data.frame(step_result$anova)
  512. # Return the final table
  513. final_anova
  514. """.format(genus_path=genus_path, metadata_path=metadata_path)
  515. # Execute the R code
  516. final_model_r = robjects.r(r_code)
  517. # Convert the R dataframe to a pandas dataframe
  518. final_model_df = rpypandas.rpy2py_dataframe(final_model_r)
  519. # Save the results
  520. final_model_df.to_csv(output_path, index=True)
  521. print(f"ordiR2step results saved to {output_path}")
  522. return final_model_df
  523. # %%
  524. from sklearn.impute import KNNImputer # We impute missing data as ordiR2step does not allow NaNs
  525. ordiR2step_dir = Path("./output/ordir2step")
  526. ordiR2step_dir.mkdir(exist_ok=True)
  527. imp = KNNImputer()
  528. imp.fit(metadata)
  529. metadata_imp = imp.transform(metadata)
  530. metadata_imp = pd.DataFrame(metadata_imp, index=metadata.index, columns=metadata.columns)
  531. metadata_imp[analysis_columns].to_csv(ordiR2step_dir/'metadata.csv',index=True)
  532. X[X.index.isin(metadata_imp.index)].to_csv(ordiR2step_dir/'microbiome.csv',index=True)
  533. genus_data_path = str(ordiR2step_dir / 'microbiome.csv')
  534. full_metadata_path = str(ordiR2step_dir / 'metadata.csv')
  535. results_output_path = str(ordiR2step_dir / 'ordir2step_results.csv')
  536. ordir2_model_results = run_ordiR2step(genus_data_path, full_metadata_path, results_output_path)
  537. # %% [markdown]
  538. # # Plot enrichment ratios and adonis results
  539. # %%
  540. from plotly import tools
  541. import plotly.graph_objs as go
  542. fig = tools.make_subplots(rows=5, cols=1, shared_xaxes=True, vertical_spacing=0.07, subplot_titles=['SAFE', 'envfit', '', 'adonis', ''])
  543. plotting_table = compared_table.copy()
  544. plotting_table.index = [idx.split("<br>")[0] for idx in plotting_table.index]
  545. plotting_table = plotting_table.sort_values(by=['enrichment_ratio'], ascending=False)
  546. # Plotting enrichment ratio
  547. fig.append_trace(
  548. go.Bar(
  549. y=plotting_table.loc[:, 'enrichment_ratio'] * 100,
  550. x=plotting_table.index,
  551. marker=dict(
  552. color=[color_codes[plotting_table.loc[fea, 'category']] for fea in plotting_table.index],
  553. line=dict(width=1)
  554. ),
  555. orientation='v',
  556. showlegend=False
  557. ), 1, 1
  558. )
  559. # Plotting envfit_adjusted_r2
  560. fig.append_trace(
  561. go.Bar(
  562. y=plotting_table.loc[:, 'envfit_adjusted_r2'],
  563. x=plotting_table.index,
  564. marker=dict(
  565. color=[
  566. color_codes[plotting_table.loc[fea, 'category']] if plotting_table.loc[fea, 'envfit_p_fdr'] < 0.05 else color_codes_light[plotting_table.loc[fea, 'category']]
  567. for fea in plotting_table.index
  568. ],
  569. line=dict(width=1)
  570. ),
  571. orientation='v',
  572. showlegend=False
  573. ), 2, 1
  574. )
  575. # Plotting envfit_neg_log_p_fdr
  576. fig.append_trace(
  577. go.Bar(
  578. y=plotting_table.loc[:, 'envfit_neg_log_p_fdr'],
  579. x=plotting_table.index,
  580. marker=dict(
  581. color=[
  582. color_codes[plotting_table.loc[fea, 'category']] if plotting_table.loc[fea, 'envfit_p_fdr'] < 0.05 else color_codes_light[plotting_table.loc[fea, 'category']]
  583. for fea in plotting_table.index
  584. ],
  585. line=dict(width=1)
  586. ),
  587. orientation='v',
  588. showlegend=False
  589. ), 3, 1
  590. )
  591. # Add a line for threshold
  592. fig.add_shape(
  593. type="line",
  594. x0=-0.5, y0=-np.log10(0.05), x1=len(plotting_table.index), y1=-np.log10(0.05),
  595. line=dict(color="gray", width=1, dash="dash"),
  596. xref='x1', yref='y3'
  597. )
  598. # Plotting adonis_adjusted_r2
  599. fig.append_trace(
  600. go.Bar(
  601. y=plotting_table.loc[:, 'adonis_adjusted_r2'],
  602. x=plotting_table.index,
  603. marker=dict(
  604. color=[
  605. color_codes[plotting_table.loc[fea, 'category']] if plotting_table.loc[fea, 'adonis_p_fdr'] < 0.05 else color_codes_light[plotting_table.loc[fea, 'category']]
  606. for fea in plotting_table.index
  607. ],
  608. line=dict(width=1)
  609. ),
  610. orientation='v',
  611. showlegend=False
  612. ), 4, 1
  613. )
  614. # Plotting adonis_neg_log_p_fdr
  615. fig.append_trace(
  616. go.Bar(
  617. y=plotting_table.loc[:, 'adonis_neg_log_p_fdr'],
  618. x=plotting_table.index,
  619. marker=dict(
  620. color=[
  621. color_codes[plotting_table.loc[fea, 'category']] if plotting_table.loc[fea, 'adonis_p_fdr'] < 0.05 else color_codes_light[plotting_table.loc[fea, 'category']]
  622. for fea in plotting_table.index
  623. ],
  624. line=dict(width=1)
  625. ),
  626. orientation='v',
  627. showlegend=False
  628. ), 5, 1
  629. )
  630. # Add a line for threshold
  631. fig.add_shape(
  632. type="line",
  633. x0=-0.5, y0=-np.log10(0.05), x1=len(plotting_table.index), y1=-np.log10(0.05),
  634. line=dict(color="gray", width=1, dash="dash"),
  635. xref='x1', yref='y5'
  636. )
  637. # Adjust layout
  638. fig.layout.xaxis1.title = ""
  639. fig.layout.yaxis1.title = "Enrichment (%)"
  640. fig.layout.yaxis2.title = "R<sup>2</sup><sub>adj</sub>"
  641. fig.layout.yaxis3.title = "-log<sub>10</sub>(p<sub>FDR</sub>)"
  642. fig.layout.yaxis4.title = "R<sup>2</sup><sub>adj</sub>"
  643. fig.layout.yaxis5.title = "-log<sub>10</sub>(p<sub>FDR</sub>)"
  644. fig.layout.margin.t = 40
  645. fig.layout.height = 600
  646. fig.layout.width = 1000
  647. fig.update_xaxes(tickangle=45)
  648. # Save the plots
  649. fig.write_html(output_dir / "enrichment_analysis/barplot_enrichment_metadata_vertical3.html")
  650. fig.write_image(output_dir / "enrichment_analysis/barplot_enrichment_metadata_vertical3.png", format="png", scale=10)
  651. fig.write_image(output_dir / "enrichment_analysis/barplot_enrichment_metadata_vertical3.svg", format="svg")
  652. fig.show()
  653. # %%
  654. # Plot barplot of enrichment ratios only
  655. from plotly import tools
  656. import plotly.graph_objs as go
  657. fig = tools.make_subplots(rows=1, cols=1, shared_xaxes=True, vertical_spacing=0.15, subplot_titles=[''])
  658. plotting_table = compared_table.copy()
  659. plotting_table.index = [idx.split("<br>")[0] for idx in plotting_table.index]
  660. plotting_table = plotting_table.sort_values(by=['enrichment_ratio'], ascending=False)
  661. # Plotting enrichment ratio
  662. fig.append_trace(
  663. go.Bar(
  664. y=plotting_table.loc[:, 'enrichment_ratio'] * 100,
  665. x=plotting_table.index,
  666. marker=dict(
  667. color=[color_codes[plotting_table.loc[fea, 'category']] for fea in plotting_table.index],
  668. line=dict(width=1)
  669. ),
  670. orientation='v',
  671. showlegend=False
  672. ), 1, 1
  673. )
  674. # Adjust layout
  675. fig.layout.xaxis1.title = ""
  676. fig.layout.yaxis1.title = "Enrichment (%)"
  677. fig.layout.margin.t = 40
  678. fig.layout.height = 350
  679. fig.layout.width = 1000
  680. fig.update_xaxes(tickangle=45)
  681. # Save the plots
  682. fig.write_html(output_dir / "enrichment_analysis/barplot_enrichment_metadata_vertical.html")
  683. fig.write_image(output_dir / "enrichment_analysis/barplot_enrichment_metadata_vertical.png", format="png", scale=10)
  684. fig.write_image(output_dir / "enrichment_analysis/barplot_enrichment_metadata_vertical.svg", format="svg")
  685. # Show the plots
  686. fig.show()
  687. # Save the resulting table
  688. compared_table.to_csv(output_dir / "enrichment_analysis/compared_bar_result_data.csv")
  689. # %% [markdown]
  690. # # Plot ordiR2step results
  691. # %%
  692. ordir2_plot_table = ordir2_model_results.copy()
  693. ordir2_plot_table = ordir2_plot_table.rename(index=variable_styling_dict_plus)
  694. individual_r2 = ordir2_plot_table['R2.adj'].diff().fillna(ordir2_plot_table['R2.adj'])
  695. variables = ordir2_plot_table.index
  696. fig = go.Figure()
  697. for i, var in enumerate(variables):
  698. legend_entry = f"{var} (+{individual_r2.iloc[i]*100:.2f}%)"
  699. fig.add_trace(go.Bar(
  700. y=[''],
  701. x=[individual_r2.iloc[i]],
  702. name=legend_entry,
  703. orientation='h'
  704. ))
  705. fig.update_layout(
  706. barmode='stack',
  707. title_text='',
  708. xaxis_title="Adjusted R²",
  709. yaxis_title="",
  710. legend_title_text="<b>Significant factors</b>",
  711. legend=dict(traceorder='normal')
  712. )
  713. fig.write_html(output_dir / "enrichment_analysis/stacked_barplot_ordir2_legend_values.html")
  714. fig.write_image(output_dir / "enrichment_analysis/stacked_barplot_ordir2_legend_values.png", format="png", scale=10)
  715. fig.write_image(output_dir / "enrichment_analysis/stacked_barplot_ordir2_legend_values.svg", format="svg")
  716. fig.show()
  717. # %% [markdown]
  718. # # Plotting enrichment landscapes
  719. # %%
  720. import plotly.graph_objs as go
  721. def plot_enrichment_landscape(
  722. graph=None, # networkx graph object | list of node position (e.g., [0.69,0.77]) as node attribute 'pos'.
  723. attribute=None,
  724. network_enrichment_scores_signif=None,
  725. fade_nonsignificant_nodes=True,
  726. variable=None, # str, variable to plot
  727. node_colormap="balance", # colormap of node coloring
  728. title=None, # title to display on the plot
  729. titlefont_size=15, # size of the title font
  730. annotation_text="", # text to display as annotation in the plot
  731. color_range_min=-5, # range of colorbar
  732. color_range_max=5, # range of colorbar
  733. nonsignif_opacity=0.4, # opacity of nonsignificant nodes
  734. node_line_width = 0.7, # width of lines around nodes
  735. node_line_color = "darkgray", # color of lines around nodes, can also be list of length n_nodes to color each node line individually
  736. show_colorbar=True, # indicate whether to display the colorbar
  737. colorbar_annotation_text="", # annotation text of the colorbar
  738. width=500, # figure width
  739. height=500, # figure height
  740. ):
  741. G = graph.copy()
  742. assert len(attribute) == len(G.nodes), "len(attribute) does not equal len(graph.nodes())"
  743. signif_idx = network_enrichment_scores_signif[network_enrichment_scores_signif[variable] == 1].index.tolist()
  744. if fade_nonsignificant_nodes == True:
  745. opacity = [1 if idx in signif_idx else nonsignif_opacity for idx in range(len(graph.nodes))]
  746. else:
  747. opacity = 1
  748. node_line_color_list = node_line_color
  749. node_text = []
  750. for idx, _ in enumerate(G.nodes):
  751. node_text.append(f'Node: {idx}<br>Value: {attribute[idx]:.3f}<br>Subjects: {",<br>".join(G.nodes[_]["sample_names"].tolist())}')
  752. edge_x = []
  753. edge_y = []
  754. for edge in G.edges():
  755. x0, y0 = G.nodes[edge[0]]['pos']
  756. x1, y1 = G.nodes[edge[1]]['pos']
  757. edge_x.append(x0)
  758. edge_x.append(x1)
  759. edge_x.append(None)
  760. edge_y.append(y0)
  761. edge_y.append(y1)
  762. edge_y.append(None)
  763. edge_trace = go.Scatter(
  764. x=edge_x, y=edge_y,
  765. line=dict(width=0.5, color='#888'),
  766. hoverinfo='none',
  767. mode='lines')
  768. node_x = []
  769. node_y = []
  770. for node in G.nodes():
  771. x, y = G.nodes[node]['pos']
  772. node_x.append(x)
  773. node_y.append(y)
  774. node_trace = go.Scatter(
  775. x=node_x, y=node_y,
  776. mode='markers',
  777. hoverinfo='text',
  778. marker=dict(
  779. showscale=show_colorbar,
  780. colorscale=node_colormap,
  781. reversescale=True,
  782. color=[],
  783. cmin=color_range_min,
  784. cmax=color_range_max,
  785. size=10,
  786. colorbar=dict(
  787. thickness=15,
  788. title=f'{colorbar_annotation_text}',
  789. xanchor='left',
  790. titleside='right',
  791. ),
  792. line_width=2))
  793. node_trace.marker.color = attribute
  794. node_trace.marker.opacity = opacity
  795. node_trace.text = node_text
  796. node_trace.marker.line["width"] = node_line_width
  797. node_trace.marker.line["color"] = node_line_color_list
  798. fig = go.Figure(data=[edge_trace, node_trace],
  799. layout=go.Layout(
  800. width=width, height=height,
  801. title={
  802. "text":f'{title}',
  803. "x":0.5,
  804. "y":0.95,
  805. },
  806. titlefont_size=titlefont_size,
  807. showlegend=False,
  808. hovermode='closest',
  809. plot_bgcolor='white', # Background color for the plotting area
  810. paper_bgcolor='white', # Background color for the entire figure
  811. margin=dict(b=20,l=5,r=5,t=40),
  812. annotations=[ dict(
  813. text=f"{annotation_text}",
  814. showarrow=False,
  815. xref="paper", yref="paper",
  816. x=0.005, y=-0.002 ) ],
  817. xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
  818. yaxis=dict(showgrid=False, zeroline=False, showticklabels=False))
  819. )
  820. return fig
  821. # %%
  822. # Plot network without annotation
  823. from plotly.offline import plot
  824. import plotly.io as pio
  825. attribute = [1] * metadata_transformed.shape[0]
  826. fig = plot_enrichment_landscape(
  827. graph=graph,
  828. attribute=attribute,
  829. fade_nonsignificant_nodes=False,
  830. network_enrichment_scores_signif=network_enrichment_scores_signif,
  831. variable="demographics_age", # pass as dummy variable so that code works
  832. node_colormap="Blues_r",
  833. title="Microbiome network",
  834. titlefont_size=40,
  835. annotation_text="",
  836. color_range_min=None,
  837. color_range_max=None,
  838. nonsignif_opacity = 0.4,
  839. node_line_color="black",
  840. node_line_width=0.3,
  841. show_colorbar=False,
  842. colorbar_annotation_text="",
  843. height=500,
  844. width=450,
  845. )
  846. fig.show()
  847. pio.write_image(fig, output_dir/f"enrichment_analysis/network.png")
  848. plot(fig, filename=Path(output_dir/f"enrichment_analysis/network.html").as_posix(), auto_open=False)
  849. fig.write_image(output_dir/f"enrichment_analysis/network.svg")
  850. # %%
  851. # Plot enrichment landscapes of non-microbiome phenotypes
  852. for variable in list(metadata.columns):
  853. attribute = network_enrichment_scores[variable]
  854. fig = plot_enrichment_landscape(
  855. graph=graph,
  856. attribute=attribute,
  857. fade_nonsignificant_nodes=True,
  858. network_enrichment_scores_signif=network_enrichment_scores_signif,
  859. variable=variable,
  860. node_colormap="balance",
  861. title=variable_styling_dict[variable].split("<br>")[0],
  862. titlefont_size=25,
  863. annotation_text="",
  864. color_range_min=-10,
  865. color_range_max=10,
  866. nonsignif_opacity = 0.4,
  867. node_line_color="black",
  868. node_line_width=0.3,
  869. show_colorbar=False,
  870. colorbar_annotation_text="",
  871. height=500,
  872. width=450,
  873. )
  874. pio.write_image(fig, output_dir/f"enrichment_landscapes/network_{variable}.png", scale=10)
  875. pio.write_image(fig, output_dir/f"enrichment_landscapes/network_{variable}.pdf", scale=10, format="pdf")
  876. fig.write_image(output_dir/f"enrichment_landscapes/network_{variable}.svg")
  877. plot(fig, filename=str(output_dir/f"enrichment_landscapes/network_{variable}.html"), auto_open=False)
  878. # %%
  879. # Plot enrichment landscapes of microbiome phenotypes
  880. for variable in list(oral_microbiome_genus.columns):
  881. attribute = network_enrichment_scores[variable]
  882. fig = plot_enrichment_landscape(
  883. graph=graph,
  884. attribute=attribute,
  885. fade_nonsignificant_nodes=True,
  886. network_enrichment_scores_signif=network_enrichment_scores_signif,
  887. variable=variable,
  888. node_colormap="balance",
  889. title=variable.split("genus_")[1],
  890. titlefont_size=25,
  891. annotation_text="",
  892. color_range_min=-10,
  893. color_range_max=10,
  894. nonsignif_opacity = 0.4,
  895. node_line_color="black",
  896. node_line_width=0.3,
  897. show_colorbar=False,
  898. colorbar_annotation_text="",
  899. height=500,
  900. width=450,
  901. )
  902. pio.write_image(fig, output_dir/f"enrichment_landscapes_genera/network_{variable}.png", scale=10)
  903. pio.write_image(fig, output_dir/f"enrichment_landscapes_genera/network_{variable}.pdf", scale=10, format="pdf")
  904. fig.write_image(output_dir/f"enrichment_landscapes_genera/network_{variable}.svg")
  905. plot(fig, filename=str(output_dir/f"enrichment_landscapes_genera/network_{variable}.html"), auto_open=False)
  906. # %%
  907. # Plot variable distribution on network of non-microbiome phenotypes
  908. for variable in list(metadata.columns):
  909. attribute = metadata_transformed[variable]
  910. attribute_max = np.nanmax(attribute)
  911. fig = plot_enrichment_landscape(
  912. graph=graph,
  913. attribute=attribute,
  914. fade_nonsignificant_nodes=False,
  915. network_enrichment_scores_signif=network_enrichment_scores_signif,
  916. variable=variable,
  917. node_colormap="balance",
  918. title=variable_styling_dict[variable].split("<br>")[0],
  919. titlefont_size=25,
  920. annotation_text="",
  921. color_range_min=attribute_max * -1,
  922. color_range_max=attribute_max,
  923. nonsignif_opacity = 0.4,
  924. node_line_color="black",
  925. node_line_width=0.3,
  926. show_colorbar=True,
  927. colorbar_annotation_text="",
  928. height=500,
  929. width=500,
  930. )
  931. pio.write_image(fig, output_dir/f"variable_landscapes/network_{variable}.png", scale=10)
  932. pio.write_image(fig, output_dir/f"variable_landscapes/network_{variable}.pdf", scale=10, format="pdf")
  933. fig.write_image(output_dir/f"variable_landscapes/network_{variable}.svg")
  934. plot(fig, filename=str(output_dir/f"variable_landscapes/network_{variable}.html"), auto_open=False)
  935. # %%
  936. # Plot variable distribution on network of microbiome phenotypes
  937. for variable in list(oral_microbiome_genus.columns):
  938. attribute = zscore(oral_microbiome_genus_transformed[variable], nan_policy="omit")
  939. attribute_max = np.nanmax(attribute)
  940. fig = plot_enrichment_landscape(
  941. graph=graph,
  942. attribute=attribute,
  943. fade_nonsignificant_nodes=False,
  944. network_enrichment_scores_signif=network_enrichment_scores_signif,
  945. variable=variable,
  946. node_colormap="balance",
  947. title=variable.split("genus_")[1],
  948. titlefont_size=25,
  949. annotation_text="",
  950. color_range_min=attribute_max * -1,
  951. color_range_max=attribute_max,
  952. nonsignif_opacity = 0.4,
  953. node_line_color="black",
  954. node_line_width=0.3,
  955. show_colorbar=True,
  956. colorbar_annotation_text="",
  957. height=500,
  958. width=500,
  959. )
  960. pio.write_image(fig, output_dir/f"variable_landscapes_genera/network_{variable}.png", scale=10)
  961. pio.write_image(fig, output_dir/f"variable_landscapes_genera/network_{variable}.pdf", scale=10, format="pdf")
  962. fig.write_image(output_dir/f"variable_landscapes_genera/network_{variable}.svg")
  963. plot(fig, filename=str(output_dir/f"variable_landscapes_genera/network_{variable}.html"), auto_open=False)
  964. # %%
  965. # Plot colorbar to use for figures
  966. cbar_fig = plot_enrichment_landscape(
  967. graph=graph,
  968. attribute=attribute,
  969. fade_nonsignificant_nodes=True,
  970. network_enrichment_scores_signif=network_enrichment_scores_signif,
  971. variable=variable,
  972. node_colormap="balance",
  973. title=variable_styling_dict[variable].split("<br>")[0],
  974. titlefont_size=25,
  975. annotation_text="",
  976. color_range_min=-10,
  977. color_range_max=10,
  978. nonsignif_opacity = 0.4,
  979. node_line_color="black",
  980. node_line_width=0.3,
  981. show_colorbar=True,
  982. colorbar_annotation_text="",
  983. height=500,
  984. width=500,
  985. )
  986. fig.write_image(output_dir/f"enrichment_landscapes/cbar_fig.svg")
  987. # %% [markdown]
  988. # # Cross-correlation matrices of network enrichment scores
  989. # %%
  990. # Microbiome phenotypes
  991. import dash_bio
  992. genus_variables = [col for col in network_enrichment_scores if "genus" in col]
  993. network_enrichment_scores_corr = network_enrichment_scores[genus_variables].corr(method="spearman")
  994. network_enrichment_scores_corr = network_enrichment_scores_corr
  995. labels = [" ".join(var.split("_")[1:]) for var in list(network_enrichment_scores_corr.index)]
  996. plot = dash_bio.Clustergram(
  997. data=network_enrichment_scores_corr,
  998. column_labels=labels,
  999. row_labels=labels,
  1000. height=1600,
  1001. width=1700,
  1002. color_map="balance_r",
  1003. cluster="all",
  1004. center_values=False,
  1005. link_method="ward"
  1006. )
  1007. heatmap_trace=plot.data[-1]
  1008. heatmap_trace.update(colorbar_title='Spearman ρ<br>')
  1009. heatmap_trace.update(colorbar_xpad= 160)
  1010. plot_dict = plot.to_dict()
  1011. plot.write_html(output_dir/"enrichment_analysis/genera_cross_correlation_matrix.html")
  1012. plot.write_image(output_dir/"enrichment_analysis/genera_cross_correlation_matrix.png", format="png", scale=10)
  1013. plot.write_image(output_dir/"enrichment_analysis/genera_cross_correlation_matrix.svg", format="svg")
  1014. # %% [markdown]
  1015. # ## Metadata
  1016. # %%
  1017. variable_styling_dict_reverse = {v:k for k,v in variable_styling_dict.items()}
  1018. signif_envfit = compared_table[compared_table["envfit_p_fdr"] < 0.05].index.tolist()
  1019. signif_envfit = [variable_styling_dict_reverse[idx] for idx in signif_envfit]
  1020. corr_df = network_enrichment_scores.drop(oral_microbiome_genus, axis=1)
  1021. corr_df = corr_df.corr(method="spearman")
  1022. # %%
  1023. # Non-microbiome phenotypes
  1024. labels = [variable_styling_dict[var].split("<br>")[0] for var in list(corr_df.index)]
  1025. plot = dash_bio.Clustergram(
  1026. data=corr_df,
  1027. column_labels=labels,
  1028. row_labels=labels,
  1029. height=800,
  1030. width=1100,
  1031. color_map="balance_r",
  1032. center_values=False,
  1033. )
  1034. # Adding heatmap to the clustergram layout
  1035. heatmap_trace=plot.data[-1]
  1036. heatmap_trace.update(colorbar_title='Spearman ρ<br><br>')
  1037. heatmap_trace.update(colorbar_xpad= 160)
  1038. plot.write_html(output_dir/"enrichment_analysis/metadata_cross_correlation_matrix.html")
  1039. plot.write_image(output_dir/"enrichment_analysis/metadata_cross_correlation_matrix.png", format="png", scale=10)
  1040. plot.write_image(output_dir/"enrichment_analysis/metadata_cross_correlation_matrix.svg", format="svg")
  1041. plot.show()
  1042. # %% [markdown]
  1043. # # Network clustering
  1044. # %%
  1045. # Perform KMeans clustering
  1046. from sklearn.cluster import KMeans
  1047. import numpy as np
  1048. positions = pd.DataFrame(nx.get_node_attributes(graph, "pos")).T
  1049. positions.columns = ["0", "1"]
  1050. clustering_input = positions.copy()
  1051. clustering_input.columns = [str(idx) for idx in list(range(clustering_input.shape[1]))]
  1052. n_clusters = 2
  1053. clustering = KMeans(n_clusters=2, random_state=42).fit(clustering_input)
  1054. positions["cluster"] = clustering.labels_
  1055. # %%
  1056. # Plot network with clustering annotation
  1057. attribute = positions["cluster"]
  1058. fig = plot_enrichment_landscape(
  1059. graph=graph,
  1060. attribute=attribute,
  1061. fade_nonsignificant_nodes=False,
  1062. network_enrichment_scores_signif=network_enrichment_scores_signif,
  1063. variable="demographics_age", # dummy variable to make the code work
  1064. node_colormap="RdBu_r",
  1065. title="",
  1066. annotation_text="",
  1067. color_range_min=0,
  1068. color_range_max=1,
  1069. nonsignif_opacity = 0.4,
  1070. node_line_color="black",
  1071. node_line_width=0.3,
  1072. show_colorbar=False,
  1073. colorbar_annotation_text="",
  1074. height=500,
  1075. width=450,
  1076. )
  1077. fig.write_html(output_dir/"cluster_analysis/graph_cluster.html")
  1078. fig.write_image(output_dir/"cluster_analysis/graph_cluster.png", format="png", scale=10)
  1079. fig.write_image(output_dir/"cluster_analysis/graph_cluster.svg", format="svg")
  1080. fig.show()
  1081. # %% [markdown]
  1082. # # Perform subject-level group comparison
  1083. # %% [markdown]
  1084. # ### Non-microbiome phenotypes
  1085. # %%
  1086. # Transfer clustering information from node to subject level
  1087. import itertools
  1088. node_subject_mapping_idx_dict = {node:list(graph.nodes[idx]["sample"]) for idx,node in enumerate(graph.nodes)}
  1089. node_subject_mapping_dict = {node:list(graph.nodes[idx]["sample_names"]) for idx,node in enumerate(graph.nodes)}
  1090. all_subject_indices = sorted(set(itertools.chain(*node_subject_mapping_dict.values())))
  1091. node_subject_df = pd.DataFrame(0, index = all_subject_indices, columns = list(graph.nodes))
  1092. for node, subjects in node_subject_mapping_dict.items():
  1093. for subject in subjects:
  1094. node_subject_df.loc[subject, node] = 1
  1095. node_subject_df = node_subject_df.loc[metadata.index[metadata.index.isin(node_subject_df.index)]]
  1096. subject_group_df = node_subject_df.T.join(positions["cluster"]).groupby("cluster").sum().T
  1097. def determine_cluster(row):
  1098. if row[0] > 0 and row[1] > 0:
  1099. return -1
  1100. elif row[0] > 0:
  1101. return 0
  1102. elif row[1] > 0:
  1103. return 1
  1104. else:
  1105. return np.nan
  1106. subject_group_df["cluster"] = subject_group_df.apply(determine_cluster, axis=1)
  1107. subject_group_df.to_csv(output_dir/"cluster_analysis/subject_clustering.csv")
  1108. # %%
  1109. statistics_df = subject_group_df["cluster"].to_frame().join(metadata_plus_imaging).join(oral_microbiome_genus)
  1110. print("n of overlapping subjects:" , len(statistics_df[statistics_df["cluster"] == -1].index))
  1111. statistics_df = statistics_df[statistics_df["cluster"] != -1]
  1112. binary_variables = [col for col in statistics_df.columns if len(statistics_df[col].unique()) == 2]
  1113. continuous_variables = [col for col in statistics_df.columns if col not in binary_variables]
  1114. # %%
  1115. # Perform group comparison of non-microbiome phenotypes
  1116. import pingouin as pg
  1117. from scipy.stats import zscore
  1118. confound_variables = [
  1119. "demographics_age",
  1120. "demographics_sex",
  1121. "demographics_education_isced",
  1122. ] + cardiovascular_risk_factors
  1123. def perform_linreg(results_df, dependent_variable):
  1124. df = statistics_df.copy()
  1125. df[confound_variables + [dependent_variable]] = zscore(df[confound_variables + [dependent_variable]], axis=0, nan_policy="omit")
  1126. model = pg.linear_regression(X=df[["cluster"] + confound_variables], y=df[dependent_variable], remove_na=True)
  1127. results_df.loc[dependent_variable,"p"] = model["pval"].values[1]
  1128. results_df.loc[dependent_variable,"coef"] = model["coef"].values[1]
  1129. results_df.loc[dependent_variable,"r2"] = model["r2"].values[1]
  1130. results_df.loc[dependent_variable,"CI[2.5%]"] = model["CI[2.5%]"].values[1]
  1131. results_df.loc[dependent_variable,"CI[97.5%]"] = model["CI[97.5%]"].values[1]
  1132. # %%
  1133. results_df = pd.DataFrame()
  1134. for dv in reversed(paro_variables + cognitive_scores+ neuropsychiatric_scores + imaging_means + inflammation + diet_scores): #
  1135. perform_linreg(results_df, dv)
  1136. results_df.index.rename("dv", inplace=True)
  1137. results_df["dv"] = results_df.index
  1138. results_df["p_fdr"] = pg.multicomp(results_df["p"].values, method="fdr_bh")[1]
  1139. # %%
  1140. gc_signif_variables = [
  1141. 'paro_cal_mean',
  1142. 'paro_dmft',
  1143. 'paro_plaqueindex',
  1144. 'paro_bop',
  1145. 'cognition_g_factor_inverted',
  1146. 'cognition_tmt_b_inverted',
  1147. 'cognition_animal_naming_test',
  1148. 'cognition_mini_mental_state_exam',
  1149. 'imaging_thickness_volume_mean',
  1150. 'inflammation_leukocytes',
  1151. ]
  1152. # %%
  1153. import plotly.graph_objects as go
  1154. import pandas as pd
  1155. from scipy.stats import zscore
  1156. between = "cluster"
  1157. # Variables and Data Preparation
  1158. plotting_variables = gc_signif_variables
  1159. statistics_df_z = statistics_df[[between] + plotting_variables].copy()
  1160. statistics_df_z[plotting_variables] = statistics_df_z[plotting_variables].apply(lambda x: zscore(x, nan_policy="omit"), axis=0)
  1161. df = pd.melt(statistics_df_z, id_vars=["cluster"], value_vars=plotting_variables)
  1162. df["variable_styled"] = [variable_styling_dict[var] for var in df["variable"]]
  1163. # Reverse the order of variables
  1164. reversed_variables = list(reversed(df['variable_styled'].unique()))
  1165. # Create the Figure
  1166. fig = go.Figure()
  1167. # Add Box Plots with offsets in y to avoid overlapping
  1168. y_offset = 0.2
  1169. for i, var in enumerate(reversed_variables):
  1170. fig.add_trace(go.Box(x=df['value'][(df[between] == 1) & (df['variable_styled'] == var)],
  1171. y=[i - y_offset] * len(df[(df[between] == 1) & (df['variable_styled'] == var)]),
  1172. name='B',
  1173. marker_color='#6ca1bc',
  1174. orientation='h',
  1175. boxpoints=False,
  1176. showlegend=(i == 0))) # Show legend only for the first variable
  1177. fig.add_trace(go.Box(x=df['value'][(df[between] == 0) & (df['variable_styled'] == var)],
  1178. y=[i + y_offset] * len(df[(df[between] == 0) & (df['variable_styled'] == var)]),
  1179. name='A',
  1180. marker_color='#b35e4b', # Darker red color code
  1181. orientation='h',
  1182. boxpoints=False,
  1183. showlegend=(i == 0))) # Show legend only for the first variable
  1184. variable = df[df['variable_styled'] == var]['variable'].unique()[0]
  1185. coef = results_df.loc[variable, "coef"]
  1186. pval = results_df.loc[variable, "p_fdr"]
  1187. if pval < 0.001:
  1188. pval_styled = ' < 0.001'
  1189. else:
  1190. pval_styled = f' = {pval:.3f}'
  1191. fig.add_annotation(x=4.5, y=i,
  1192. text=f"β<sub>std</sub> = {coef:.2f}", # <br>p{pval_styled}
  1193. showarrow=False)
  1194. if pval < 0.001: asterisk = "***"
  1195. elif pval < 0.01: asterisk = "**"
  1196. elif pval < 0.05: asterisk = "*"
  1197. else: asterisk = ""
  1198. fig.add_annotation(x=2.5, y=i-0.05,
  1199. text=asterisk,
  1200. showarrow=False,
  1201. font=dict(size=18))
  1202. # Update Layout
  1203. fig.update_layout(
  1204. template="plotly_white",
  1205. title='',
  1206. xaxis_title="z",
  1207. yaxis_title="",
  1208. xaxis=dict(range=[-3, 4.5],
  1209. tickvals=[-2.5, 0, 2.5]), # Set x-axis limits
  1210. yaxis=dict(
  1211. tickvals=list(range(len(reversed_variables))),
  1212. ticktext=reversed_variables,
  1213. ),
  1214. width=480,
  1215. height=900,
  1216. legend=dict(
  1217. title="",
  1218. x=-12, # Adjust the x position
  1219. y=-10, # Adjust the y position
  1220. xanchor="left", # Horizontal anchor at 'left'
  1221. yanchor="middle", # Vertical anchor at 'middle'
  1222. )
  1223. )
  1224. fig.write_html(output_dir/"cluster_analysis/group_comparison_brain_health_box.html")
  1225. fig.write_image(output_dir/"cluster_analysis/group_comparison_brain_health_box.png", format="png", scale=10)
  1226. fig.write_image(output_dir/"cluster_analysis/group_comparison_brain_health_box.svg", format="svg")
  1227. fig.show()
  1228. # %% [markdown]
  1229. # ### Confound analysis
  1230. # %%
  1231. confound_variables = ['demographics_age',
  1232. 'demographics_sex',
  1233. 'demographics_education_isced',
  1234. 'cvrisk_systolic_blood_pressure_mmhg',
  1235. 'cvrisk_diastolic_blood_pressure_mmhg',
  1236. 'cvrisk_BMI',
  1237. 'cvrisk_smoking_currently',
  1238. 'blood_cholesterol_mg_dl',
  1239. 'blood_hdl_mg_dl',
  1240. 'blood_ldl_mg_dl',
  1241. 'blood_triglycerides_mg_dl',
  1242. 'blood_hba1c']
  1243. # %%
  1244. def perform_linreg(results_df, dependent_variable, covariates):
  1245. df = statistics_df.copy()
  1246. if covariates == None:
  1247. df[dependent_variable] = zscore(df[dependent_variable], axis=0, nan_policy="omit")
  1248. model = pg.linear_regression(X=df["cluster"], y=df[dependent_variable], remove_na=True)
  1249. elif len(covariates) == 1:
  1250. df[covariates + [dependent_variable]] = zscore(df[covariates + [dependent_variable]], axis=0, nan_policy="omit")
  1251. model = pg.linear_regression(X=df[["cluster"] + covariates], y=df[dependent_variable], remove_na=True)
  1252. elif len(covariates) > 1:
  1253. df[covariates + [dependent_variable]] = zscore(df[covariates + [dependent_variable]], axis=0, nan_policy="omit")
  1254. model = pg.linear_regression(X=df[["cluster"] + covariates], y=df[dependent_variable], remove_na=True)
  1255. results_df.loc[dependent_variable,"p"] = model["pval"].values[1]
  1256. results_df.loc[dependent_variable,"coef"] = model["coef"].values[1]
  1257. results_df.loc[dependent_variable,"r2"] = model["r2"].values[1]
  1258. results_df.loc[dependent_variable,"CI[2.5%]"] = model["CI[2.5%]"].values[1]
  1259. results_df.loc[dependent_variable,"CI[97.5%]"] = model["CI[97.5%]"].values[1]
  1260. return results_df
  1261. # %%
  1262. p_df = pd.DataFrame()
  1263. coef_df = pd.DataFrame()
  1264. r2_df = pd.DataFrame()
  1265. for idx, dv in enumerate(paro_variables + inflammation + cognitive_scores+ neuropsychiatric_scores + imaging_means + diet_scores + confound_variables):
  1266. confound_iteration = []
  1267. confound_analysis_df_iter = pd.DataFrame()
  1268. dv_styled = variable_styling_dict[dv]
  1269. if "<br>" in dv_styled: dv_styled = dv_styled.split("<br>")[0]
  1270. confound_analysis_df_iter = perform_linreg(confound_analysis_df_iter, dv, covariates=None)
  1271. p_df.loc[dv_styled,"Unadjusted"] = confound_analysis_df_iter.iloc[0,0]
  1272. coef_df.loc[dv_styled,"Unadjusted"] = confound_analysis_df_iter.iloc[0,1]
  1273. r2_df.loc[dv_styled,"Unadjusted"] = confound_analysis_df_iter.iloc[0,2]
  1274. for confounder in confound_variables:
  1275. if dv == confounder: continue
  1276. confounder_styled = variable_styling_dict[confounder]
  1277. confound_iteration.append(confounder)
  1278. confound_analysis_df_iter = pd.DataFrame()
  1279. confound_analysis_df_iter = perform_linreg(confound_analysis_df_iter, dv, covariates=confound_iteration)
  1280. p_df.loc[dv_styled,"+ " + confounder_styled] = confound_analysis_df_iter.iloc[0,0]
  1281. coef_df.loc[dv_styled,"+ " + confounder_styled] = confound_analysis_df_iter.iloc[0,1]
  1282. r2_df.loc[dv_styled,"+ " + confounder_styled] = confound_analysis_df_iter.iloc[0,2]
  1283. # %%
  1284. import plotly.graph_objects as go
  1285. annotation_df = p_df.applymap(lambda p: '*' if p < 0.05 else '')
  1286. fig = go.Figure(data=go.Heatmap(
  1287. z=coef_df.values,
  1288. x=coef_df.columns,
  1289. y=coef_df.index,
  1290. text=annotation_df.values,
  1291. texttemplate="%{text}",
  1292. textfont={"size": 18, "color": "darkred"},
  1293. colorscale='RdBu_r',
  1294. zmid=0,
  1295. colorbar=dict(title='β<sub>std</sub>')
  1296. ))
  1297. fig.update_layout(
  1298. title='',
  1299. xaxis_title='',
  1300. yaxis_title='',
  1301. xaxis=dict(tickangle=-45),
  1302. yaxis=dict(autorange='reversed'),
  1303. width=900,
  1304. height=800,
  1305. margin=dict(l=250, r=50, b=150, t=50)
  1306. )
  1307. fig.show()
  1308. fig.write_html(output_dir/"cluster_analysis/confound_analysis.html")
  1309. fig.write_image(output_dir/"cluster_analysis/confound_analysis.png", format="png", scale=10)
  1310. fig.write_image(output_dir/"cluster_analysis/confound_analysis.svg", format="svg")
  1311. # %% [markdown]
  1312. # ### Genera
  1313. # %%
  1314. from scipy.stats import zscore
  1315. from skbio.stats.composition import clr
  1316. microbiome_non_zero = oral_microbiome_genus + 1
  1317. clr_data = clr(microbiome_non_zero)
  1318. clr_df = pd.DataFrame(clr_data,
  1319. index=oral_microbiome_genus.index,
  1320. columns=oral_microbiome_genus.columns)
  1321. microbiome_z_columns = []
  1322. for dv in clr_df.columns:
  1323. z_col_name = f"{dv}_z"
  1324. statistics_df[z_col_name] = zscore(clr_df[dv], axis=0, nan_policy="omit")
  1325. microbiome_z_columns.append(z_col_name)
  1326. # %%
  1327. results_df = pd.DataFrame()
  1328. for dv in microbiome_z_columns:
  1329. perform_linreg(results_df, dv)
  1330. results_df.index.rename("dv", inplace=True)
  1331. results_df["dv"] = results_df.index
  1332. results_df["p_fdr"] = pg.multicomp(results_df["p"].values, method="fdr_bh")[1]
  1333. # %%
  1334. highest_change_pos = results_df[(results_df["p_fdr"]< 0.05) & (results_df["coef"]> 0)].sort_values(by="coef", ascending=False).iloc[:15].index.to_list()
  1335. highest_change_neg = results_df[(results_df["p_fdr"]< 0.05) & (results_df["coef"]< 0)].sort_values(by="coef").iloc[:15].index.to_list()
  1336. highest_change_pos = [var.split("_z")[0] for var in highest_change_pos]
  1337. highest_change_neg = [var.split("_z")[0] for var in highest_change_neg]
  1338. # %%
  1339. genera_signif = results_df[results_df["p_fdr"]< 0.05].sort_values(by="coef", ascending=False).index.to_list()
  1340. genera_signif = [var.split("_z")[0] for var in genera_signif]
  1341. # %%
  1342. import pandas as pd
  1343. import plotly.graph_objects as go
  1344. import plotly.express as px
  1345. # Filter the top 15 positive and top 15 negative coefficients
  1346. df_sorted = results_df.sort_values('coef', ascending=True)
  1347. df_top_pos = df_sorted.head(15)
  1348. df_top_neg = df_sorted.tail(15)
  1349. df_top = pd.concat([df_top_pos, df_top_neg])
  1350. colorscale = px.colors.qualitative.Prism + px.colors.qualitative.Plotly + px.colors.qualitative.Pastel # Choose your desired colorscale
  1351. # Define significance levels
  1352. def significance_indicator(p_value):
  1353. if p_value < 0.001:
  1354. return '***'
  1355. elif p_value < 0.01:
  1356. return '**'
  1357. elif p_value < 0.05:
  1358. return '*'
  1359. else:
  1360. return ''
  1361. df_top['significance'] = df_top['p'].apply(significance_indicator)
  1362. df_top['genus'] = df_top['dv'].apply(lambda x: " ".join(x.split('_')[1:]).split(" z")[0]) # Simplify genus names if necessary
  1363. # Create the bar plot
  1364. fig = go.Figure()
  1365. # Add bars
  1366. fig.add_trace(go.Bar(
  1367. x=df_top['genus'],
  1368. y=df_top['coef'],
  1369. error_y=dict(
  1370. type='data',
  1371. symmetric=False,
  1372. array=df_top['CI[97.5%]'] - df_top['coef'],
  1373. arrayminus=df_top['coef'] - df_top['CI[2.5%]']
  1374. ),
  1375. marker=dict(color=df_top['coef'], colorscale='RdBu'), # dict(color=colorscale),#
  1376. hoverinfo='x+y',
  1377. ))
  1378. # Add significance annotations
  1379. for i, row in df_top.iterrows():
  1380. if row['significance']:
  1381. fig.add_annotation(y=row["coef"] + 0.4 if row['coef'] > 0 else row["coef"] -0.4, x=row['genus'],
  1382. text=row['significance'],
  1383. showarrow=False, font=dict(color='black'), textangle=0)
  1384. fig.add_annotation(x=2.5, y=1.5,
  1385. text="*:p<sub>FDR</sub><0.05<br>**:p<sub>FDR</sub><0.01<br>***:p<sub>FDR</sub><0.001",
  1386. showarrow=False, font=dict(color='black'))
  1387. # Horizontal line at y=0
  1388. fig.add_hline(y=0, line=dict(color="grey", width=1))
  1389. # Update layout
  1390. fig.update_layout(
  1391. title="",
  1392. xaxis_title="",
  1393. yaxis_title="β<sub>std</sub>",
  1394. template="plotly_white",
  1395. height=400,
  1396. width=850,
  1397. xaxis=dict(tickangle=45) # Rotate the x-ticks by -45 degrees
  1398. )
  1399. fig.write_html(output_dir/"cluster_analysis/abundancy_cluster_barplot_coefficients.html")
  1400. fig.write_image(output_dir/"cluster_analysis/abundancy_cluster_barplot_coefficients.png", format="png", scale=10)
  1401. fig.write_image(output_dir/"cluster_analysis/abundancy_cluster_barplot_coefficients.svg", format="svg")
  1402. # Show the plot
  1403. fig.show()
  1404. # %%
  1405. df_sorted = results_df.sort_values('coef', ascending=True)
  1406. df_top = df_sorted
  1407. df_top['genus'] = df_top['dv'].apply(lambda x: " ".join(x.split('_')[1:]).split(" z")[0]) # Simplify genus names if necessary
  1408. # Create the horizontal bar plot
  1409. fig = go.Figure()
  1410. # Add bars with orientation set to horizontal
  1411. fig.add_trace(go.Bar(
  1412. y=df_top['genus'], # Use y for horizontal bar plot
  1413. x=df_top['coef'], # Use x for horizontal bar plot
  1414. error_x=dict( # Adjust error_x instead of error_y for horizontal errors
  1415. type='data',
  1416. symmetric=False,
  1417. array=df_top['CI[97.5%]'] - df_top['coef'],
  1418. arrayminus=df_top['coef'] - df_top['CI[2.5%]'],
  1419. width=1
  1420. ),
  1421. marker=dict(color=df_top['coef'], colorscale='RdBu'), # Update marker color
  1422. hoverinfo='y+x', # Adjust hover info for horizontal orientation
  1423. orientation='h' # Set orientation to horizontal
  1424. ))
  1425. # Vertical line at x=0
  1426. fig.add_vline(x=0, line=dict(color="grey", width=1))
  1427. # Update layout for horizontal bar plot
  1428. fig.update_layout(
  1429. title="",
  1430. yaxis_title="", # Update y-axis title for horizontal display
  1431. xaxis_title="β<sub>std</sub>",
  1432. template="plotly_white",
  1433. height=1400, # Adjust height for horizontal plot
  1434. width=700, # Adjust width for horizontal plot
  1435. )
  1436. fig.write_html(output_dir/"cluster_analysis/abundancy_cluster_barplot_coefficients_all.html")
  1437. fig.write_image(output_dir/"cluster_analysis/abundancy_cluster_barplot_coefficients_all.png", format="png", scale=10)
  1438. fig.write_image(output_dir/"cluster_analysis/abundancy_cluster_barplot_coefficients_all.svg", format="svg")
  1439. # Save or display the horizontal bar plot
  1440. fig.show()
  1441. # %%
  1442. import plotly.graph_objects as go
  1443. import pandas as pd
  1444. import plotly.colors as pcolors # Import Plotly color scales
  1445. import plotly.express as px
  1446. # Define a custom color palette (e.g., using Plotly's predefined color scales)
  1447. colorscale = px.colors.qualitative.Prism + px.colors.qualitative.Plotly + px.colors.qualitative.Pastel # Choose your desired colorscale
  1448. genera_cols = genera_signif
  1449. # Copying and transforming the dataset
  1450. plotting_df = statistics_df[["cluster"] + highest_change_pos + highest_change_neg].copy()
  1451. genera_cols_styled = ["_".join(idx.split("_")[1:]) for idx in highest_change_pos + highest_change_neg]
  1452. plotting_df.columns = ["cluster"] + genera_cols_styled
  1453. plotting_df[genera_cols_styled] = plotting_df[genera_cols_styled].div(statistics_df[oral_microbiome_genus.columns].sum(axis=1), axis=0)
  1454. # Melting the dataframe for long-form plotting
  1455. plotting_df_0 = plotting_df[plotting_df["cluster"] == 0].mean(axis=0)
  1456. plotting_df_1 = plotting_df[plotting_df["cluster"] == 1].mean(axis=0)
  1457. # Sort each cluster's values in descending order
  1458. plotting_df_0 = plotting_df_0.sort_values(ascending=False)
  1459. plotting_df_1 = plotting_df_1.sort_values(ascending=False)
  1460. # Combine into a DataFrame
  1461. plotting_df = pd.concat([plotting_df_0, plotting_df_1], axis=1).T
  1462. plotting_df = plotting_df * 100
  1463. plotting_df["cluster"] = [0, 1]
  1464. # Reorder columns based on sorted indexes for cluster 0 and cluster 1
  1465. sorted_columns = plotting_df_0.drop("cluster").index.tolist()
  1466. # Creating the main Figure
  1467. fig = go.Figure()
  1468. # Looping through each genus in sorted order to add a bar trace
  1469. color_idx = 0
  1470. for genus in sorted_columns:
  1471. color = colorscale[color_idx % len(colorscale)] # Get color from colorscale
  1472. color_idx += 1
  1473. fig.add_trace(go.Bar(
  1474. x=plotting_df["cluster"],
  1475. y=plotting_df[genus],
  1476. name=genus,
  1477. text=genus if plotting_df[genus].max() > 1 else "", # Only show text if the max value is greater than 1
  1478. hovertemplate=f"% {genus}<br>" + "%{y:.3f}<extra></extra>", # Display genus name on hover
  1479. marker_color=color, # Assign color to the trace
  1480. ))
  1481. # Updating layout to match the original requirements
  1482. fig.update_layout(
  1483. template="plotly_white",
  1484. height=600,
  1485. width=550,
  1486. barmode='stack',
  1487. xaxis_title="Cluster",
  1488. yaxis_title="Abundance (%)",
  1489. title="",
  1490. showlegend=True,
  1491. )
  1492. # Customizing x-axis to show only 0 and 1
  1493. fig.update_xaxes(
  1494. tickvals=[0, 1],
  1495. ticktext=["A", "B"]
  1496. )
  1497. # save html
  1498. fig.write_html(output_dir/"cluster_analysis/abundancy_cluster_high_diff_genera.html")
  1499. fig.write_image(output_dir/"cluster_analysis/abundancy_cluster_high_diff_genera.png", format="png", scale=10)
  1500. fig.write_image(output_dir/"cluster_analysis/abundancy_cluster_high_diff_genera.svg", format="svg")
  1501. # Showing the plot
  1502. fig.show()
  1503. # %%
  1504. import plotly.graph_objects as go
  1505. import pandas as pd
  1506. import plotly.colors as pcolors # Import Plotly color scales
  1507. import plotly.express as px
  1508. # Define a custom color palette (e.g., using Plotly's predefined color scales)
  1509. colorscale = px.colors.qualitative.Prism + px.colors.qualitative.Plotly + px.colors.qualitative.Pastel # Choose your desired colorscale
  1510. genera_cols = genera_signif
  1511. # Copying and transforming the dataset
  1512. plotting_df = statistics_df[["cluster"] + genera_cols].copy()
  1513. genera_cols_styled = ["_".join(idx.split("_")[1:]) for idx in genera_cols]
  1514. plotting_df.columns = ["cluster"] + genera_cols_styled
  1515. plotting_df[genera_cols_styled] = plotting_df[genera_cols_styled].div(plotting_df[genera_cols_styled].sum(axis=1), axis=0)
  1516. # Melting the dataframe for long-form plotting
  1517. plotting_df_0 = plotting_df[plotting_df["cluster"] == 0].mean(axis=0)
  1518. plotting_df_1 = plotting_df[plotting_df["cluster"] == 1].mean(axis=0)
  1519. # Sort each cluster's values in descending order
  1520. plotting_df_0 = plotting_df_0.sort_values(ascending=False)
  1521. plotting_df_1 = plotting_df_1.sort_values(ascending=False)
  1522. # Combine into a DataFrame
  1523. plotting_df = pd.concat([plotting_df_0, plotting_df_1], axis=1).T
  1524. plotting_df = plotting_df * 100
  1525. plotting_df["cluster"] = [0, 1]
  1526. # Reorder columns based on sorted indexes for cluster 0 and cluster 1
  1527. sorted_columns = plotting_df_0.drop("cluster").index.tolist()
  1528. # Creating the main Figure
  1529. fig = go.Figure()
  1530. # Looping through each genus in sorted order to add a bar trace
  1531. color_idx = 0
  1532. for genus in sorted_columns:
  1533. color = colorscale[color_idx % len(colorscale)] # Get color from colorscale
  1534. color_idx += 1
  1535. fig.add_trace(go.Bar(
  1536. x=plotting_df["cluster"],
  1537. y=plotting_df[genus],
  1538. name=genus,
  1539. text=genus if plotting_df[genus].max() > 2 else "", # Only show text if the max value is greater than 1
  1540. hovertemplate=f"% {genus}<br>" + "%{y:.3f}<extra></extra>", # Display genus name on hover
  1541. marker_color=color, # Assign color to the trace
  1542. ))
  1543. # Updating layout to match the original requirements
  1544. fig.update_layout(
  1545. template="plotly_white",
  1546. height=900,
  1547. width=600,
  1548. barmode='stack',
  1549. xaxis_title="Cluster",
  1550. yaxis_title="Abundance (%)",
  1551. title="",
  1552. showlegend=False,
  1553. )
  1554. # Customizing x-axis to show only 0 and 1
  1555. fig.update_xaxes(
  1556. tickvals=[0, 1],
  1557. ticktext=["A", "B"]
  1558. )
  1559. # save html
  1560. fig.write_html(output_dir/"cluster_analysis/abundancy_cluster_all_genera.html")
  1561. fig.write_image(output_dir/"cluster_analysis/abundancy_cluster_all_genera.png", format="png", scale=10)
  1562. fig.write_image(output_dir/"cluster_analysis/abundancy_cluster_all_genera.svg", format="svg")
  1563. # Showing the plot
  1564. fig.show()
  1565. # %% [markdown]
  1566. # # Dominant genera
  1567. # %%
  1568. # modified from tmap documentation https://tmap.readthedocs.io/en/latest/
  1569. import plotly.graph_objects as go
  1570. from collections import Counter
  1571. import numpy as np
  1572. import plotly.subplots
  1573. import plotly.express as px
  1574. def plot_dominance_landscape(
  1575. graph=None,
  1576. dominance_df=None,
  1577. thresh_top_enrichment=5, # min amount of top enrichment nodes a variable must have to plot it
  1578. colormap=px.colors.qualitative.Prism, # Using a Plotly colorscale
  1579. node_line_color="black",
  1580. node_line_width=0.3, # node line width
  1581. opacity=0.9, # node opacity
  1582. width=900,
  1583. height=700,
  1584. ):
  1585. # Assuming 'pos' is an attribute in the graph nodes that stores position
  1586. node_pos = np.array([graph.nodes[node]['pos'] for node in graph.nodes()])
  1587. # Edges for the graph
  1588. xs, ys = [], []
  1589. for edge in graph.edges:
  1590. xs += [node_pos[edge[0]][0], node_pos[edge[1]][0], None]
  1591. ys += [node_pos[edge[0]][1], node_pos[edge[1]][1], None]
  1592. fig = plotly.subplots.make_subplots(rows=1, cols=1)
  1593. # Add edges to the plot
  1594. fig.add_trace(go.Scatter(x=xs, y=ys, mode="lines",
  1595. line=dict(width=1, color="#8E9DA2"), showlegend=False), row=1, col=1)
  1596. # Calculate which feature is most dominant per node
  1597. feature_indices = np.argmax(dominance_df.values, axis=1)
  1598. tmp = [dominance_df.columns[index] for index in feature_indices]
  1599. # Count the number of top enrichment nodes per variable
  1600. t = Counter(tmp)
  1601. enrichment_features = {fea for fea, count in t.items() if count >= thresh_top_enrichment}
  1602. cmap = {feature: colormap[i % len(colormap)] for i, feature in enumerate(enrichment_features)}
  1603. # Plot nodes based on the dominant feature and use colormap for nodes
  1604. for feature in enrichment_features:
  1605. indices = [i for i, f in enumerate(tmp) if f == feature]
  1606. fig.add_trace(go.Scatter(
  1607. x=node_pos[indices, 0], y=node_pos[indices, 1], mode='markers',
  1608. marker=dict(size=15, color=cmap[feature], opacity=opacity, line=dict(width=node_line_width, color=node_line_color)),
  1609. name=f'{variable_styling_dict[feature].split("<br>")[0] if feature in variable_styling_dict.keys() else feature.replace("_"," ")} ({t[feature]})',
  1610. showlegend=True
  1611. ), row=1, col=1)
  1612. # Update layout
  1613. fig.update_layout(
  1614. width=width, height=height, hovermode='closest', plot_bgcolor="white", paper_bgcolor="white",
  1615. xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
  1616. yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
  1617. )
  1618. return fig
  1619. # %%
  1620. dominance_df = network_enrichment_scores[oral_microbiome_genus.columns]
  1621. colormap = plotly.colors.qualitative.Prism + plotly.colors.qualitative.Plotly
  1622. dominance_df.columns = [" ".join(col.split("_")[1:]) for col in dominance_df.columns]
  1623. fig = plot_dominance_landscape(
  1624. graph=graph,
  1625. dominance_df=dominance_df,
  1626. colormap=colormap,
  1627. thresh_top_enrichment=5,
  1628. node_line_color="black",
  1629. node_line_width=0.3,
  1630. opacity=0.9,
  1631. width=800,
  1632. height=700,
  1633. )
  1634. fig.write_html(output_dir/"enrichment_analysis/dominance_all_genera.html")
  1635. fig.write_image(output_dir/"enrichment_analysis/dominance_all_genera.svg")
  1636. fig.write_image(output_dir/"enrichment_analysis/dominance_all_genera.png", format="png", scale=10)
  1637. fig.show()

analysis.ipynb at commit 5fee720, no license · at the source

Overview

Authors: Marvin Petersen1, Carolin Walther2, Katrin Borof2, Guido Heydecke3, Thomas Beikler2, Malik Alawi4, Christian Müller4, Felix L. Nägele1, Birgit-Christiane Zyriax5, Jens Fiehler6, Jürgen Gallinat7, Simone Kühn7,8, Raphael Twerenbold9,10,11,12, Corinna Bang13, Götz Thomalla1, Bastian Cheng1, Ghazal Aarabi2
13 affiliations
  1. Department of Neurology, University Medical Centre Hamburg-Eppendorf, Hamburg, Germany
  2. Department of Periodontics, Preventive and Restorative Dentistry, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  3. Department of Prosthetic Dentistry, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  4. Bioinformatics Core, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  5. Midwifery Science-Health Services Research and Prevention, Institute for Health Services Research in Dermatology and Nursing (IVDP), University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  6. Department of Neuroradiology, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  7. Department of Psychiatry and Psychotherapy, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  8. Lise Meitner Group for Environmental Neuroscience, Max Planck Institute for Human Development, Berlin, Germany
  9. Department of Cardiology, University Heart and Vascular Center, Hamburg, Germany
  10. Epidemiological Study Center, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  11. German Center for Cardiovascular Research (DZHK), Partner Site Hamburg/Kiel/Luebeck, Hamburg, Germany
  12. University Center of Cardiovascular Science, University Heart and Vascular Center, Hamburg, Germany
  13. Institute of Clinical Molecular Biology, Kiel University, Kiel, Germany
Journal: EBioMedicine, volume 128, article 106312
Dates: received 28 December 2025; accepted 12 May 2026; published online 30 May 2026; in print June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.ebiom.2026.106312 · PMID 42217285 · PMCID PMC13241667 · OpenAlex W7162878770
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), cognitive (subfield)
Methods: Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, Graphs, fMRI & imaging
Keywords: Oral microbiome, Periodontitis, Oral-brain axis, Brain health, Cognition, Neuroimaging
MeSH: Brain*, Gingiva*, Microbiota*, Periodontitis*, Aged, Bacteria, Biomarkers, Cognition, Female, Humans, Male, Middle Aged, RNA, Ribosomal, 16S (* major topic)
Topic: Gut microbiota and health (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Citations: not cited yet (Europe PMC); 67 references in the paper

Abstract

Background: Periodontitis has gained attention as a key factor associated with cognitive decline and Alzheimer's dementia. However, the relationship between periodontitis-related oral microbiota shifts and brain health in the general population remains unclear.

Methods: We investigated the oral microbiome–brain axis in 1026 participants from the population-based PAROMIND Study. Using 16S rRNA gene amplicon sequencing of subgingival crevicular fluid, we inferred via topological data analysis a microbiota similarity network. This network, which distills the complex high-dimensional data into an interpretable map of microbial similarity, revealed a continuous disease gradient mirroring the microbial pathogenicity spectrum, from taxa of low periodontal pathogenicity (e.g., Streptococcus) to periodontitis-associated taxa (e.g., Porphyromonas, Fusobacterium). Leveraging this network, we systematically examined associations between periodontal microbiota profiles and 40 brain health-related phenotypes, including cognition, brain structure, mental health, inflammatory biomarkers, diet, vascular risk factors, and demographics.

Findings: Higher abundance of periodontitis-related bacterial taxa was associated with poorer cognitive performance, elevated leucocyte counts, and lower MIND diet adherence after covariate adjustment. Complementary forward model selection analysis supported the links to cognitive performance and inflammation, and additionally identified a significant association with brain structure (cortical thickness and subcortical volume). We identified associations with both established genera (Porphyromonas) and taxa not previously implicated in brain health (Fretibacterium, Tannerella, Dialister).

Interpretation: These findings from a large cohort advance the understanding of the oral microbiome–brain axis, highlighting specific microbial profiles linked to subclinical cognitive, structural, and inflammatory brain health markers. By demonstrating these links in a non-demented population, our study suggests that monitoring the oral microbiome could inform early risk assessment for cognitive decline, positioning periodontal health as an accessible target for early intervention strategies.

Funding: Deutsche Forschungsgemeinschaft.

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

Repository

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

csi-hamburg/oral_microbiome_brain_health

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 5fee7207cc6aa54a16c588f93bb9b6f9ad418497, 1 June 2026
Languages: Jupyter (1)
Size: 2 files, 1 script
Software Heritage: not archived
Found in: “Data sharing statement”
Holds: README, 1 notebook
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NetworkX (1 file), NumPy (1 file), pandas (1 file), Pingouin (1 file), Plotly (1 file), rpy2 (1 file), scikit-learn (1 file), SciPy (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
2 files

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

Tracing map

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

What the map holds:

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

Sequencing data generated during this study have been deposited as FASTQ files in the European Nucleotide Archive (ENA) under accession number PRJEB89258. Corresponding non-microbiome phenotype data from the HCHS are not publicly available due to data protection policies that ensure participant confidentiality. However, this data is available to qualified researchers upon reasonable request to the HCHS steering committee. The analysis code for this work is publicly available on GitHub (https://github.com/csi-hamburg/oral_microbiome_brain_health). Interactive versions of the plots can be found on OSF (https://osf.io/vqj8m/).

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

Versions

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

Version 2, 28 September 2026

  • Authors: added Bastian Cheng (0000-0003-2434-1822); Ghazal Aarabi (0000-0001-5484-2594); removed Bastian Cheng; Ghazal Aarabi

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 17 authors, 6 keywords, 13 MeSH terms, 1 funder, 66 references.

Cite

This paper

Petersen, M., Walther, C., Borof, K., Heydecke, G., Beikler, T., Alawi, M., Müller, C., Nägele, F. L., Zyriax, B.-C., Fiehler, J., Gallinat, J., Kühn, S., Twerenbold, R., Bang, C., Thomalla, G., Cheng, B., & Aarabi, G. (2026). Subgingival microbiota composition is associated with brain health in the general population-the PAROMIND study. EBioMedicine, 128, 106312. https://doi.org/10.1016/j.ebiom.2026.106312

BibTeX

@article{petersen2026subgingival,
author = {Petersen, Marvin and Walther, Carolin and Borof, Katrin and Heydecke, Guido and Beikler, Thomas and Alawi, Malik and Müller, Christian and Nägele, Felix L. and Zyriax, Birgit-Christiane and Fiehler, Jens and Gallinat, Jürgen and Kühn, Simone and Twerenbold, Raphael and Bang, Corinna and Thomalla, Götz and Cheng, Bastian and Aarabi, Ghazal},
title = {{Subgingival microbiota composition is associated with brain health in the general population-the PAROMIND study}},
journal = {EBioMedicine},
year = {2026},
month = may,
volume = {128},
pages = {106312},
publisher = {Elsevier},
issn = {2352-3964},
doi = {10.1016/j.ebiom.2026.106312},
url = {https://doi.org/10.1016/j.ebiom.2026.106312},
pmid = {42217285},
pmcid = {PMC13241667}
}

RIS

TY - JOUR
AU - Petersen, Marvin
AU - Walther, Carolin
AU - Borof, Katrin
AU - Heydecke, Guido
AU - Beikler, Thomas
AU - Alawi, Malik
AU - Müller, Christian
AU - Nägele, Felix L.
AU - Zyriax, Birgit-Christiane
AU - Fiehler, Jens
AU - Gallinat, Jürgen
AU - Kühn, Simone
AU - Twerenbold, Raphael
AU - Bang, Corinna
AU - Thomalla, Götz
AU - Cheng, Bastian
AU - Aarabi, Ghazal
TI - Subgingival microbiota composition is associated with brain health in the general population-the PAROMIND study
T2 - EBioMedicine
J2 - EBioMedicine
PY - 2026
DA - 2026/05/30
VL - 128
SP - 106312
SN - 2352-3964
PB - Elsevier
DO - 10.1016/j.ebiom.2026.106312
UR - https://doi.org/10.1016/j.ebiom.2026.106312
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.ebiom.2026.106312",
"type": "article-journal",
"title": "Subgingival microbiota composition is associated with brain health in the general population-the PAROMIND study",
"container-title": "EBioMedicine",
"author": [
{
"family": "Petersen",
"given": "Marvin"
},
{
"family": "Walther",
"given": "Carolin"
},
{
"family": "Borof",
"given": "Katrin"
},
{
"family": "Heydecke",
"given": "Guido"
},
{
"family": "Beikler",
"given": "Thomas"
},
{
"family": "Alawi",
"given": "Malik"
},
{
"family": "Müller",
"given": "Christian"
},
{
"family": "Nägele",
"given": "Felix L."
},
{
"family": "Zyriax",
"given": "Birgit-Christiane"
},
{
"family": "Fiehler",
"given": "Jens"
},
{
"family": "Gallinat",
"given": "Jürgen"
},
{
"family": "Kühn",
"given": "Simone"
},
{
"family": "Twerenbold",
"given": "Raphael"
},
{
"family": "Bang",
"given": "Corinna"
},
{
"family": "Thomalla",
"given": "Götz"
},
{
"family": "Cheng",
"given": "Bastian"
},
{
"family": "Aarabi",
"given": "Ghazal"
}
],
"container-title-short": "EBioMedicine",
"volume": "128",
"page": "106312",
"DOI": "10.1016/j.ebiom.2026.106312",
"PMID": "42217285",
"PMCID": "PMC13241667",
"ISSN": "2352-3964",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.ebiom.2026.106312",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
30
]
]
}
}

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.3389/fnagi.2026.1789408 [code]
Biological brain aging, cognitive-motor decline and vascular risk: a multivariate imaging analysis of 40,579 individuals.
Journal: Frontiers in aging neuroscience
In common: scikit-learn, pandas, SciPy, 1 other tool, cognitive, 4 references
[2] doi:10.7554/elife.103097 [code]
Canonical neurodevelopmental trajectories of structural and functional manifolds.
Journal: eLife
In common: rpy2, Pingouin, Plotly, 5 other tools
[3] 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: rpy2, NetworkX, Plotly, 4 other tools, 1 reference
[4] doi:10.1371/journal.pcbi.1014346 [code]
StPedf: Cell trajectory inference of spatial transcriptomics via spatial proximity embedding and spatial density-adaptive fusion.
Journal: PLoS computational biology
In common: rpy2, NetworkX, Plotly, 5 other tools
[5] doi:10.1016/j.isci.2026.116055 [code]
Mapping the transcriptional diversity of calcium signaling in the mouse and human brain.
Journal: iScience
In common: rpy2, NetworkX, Plotly, 5 other tools
[6] doi:10.1038/s41593-026-02267-3 [code]
Spatial proteomic analysis in human Alzheimer's disease brains enables identification of microenvironment-dependent microglial cell states.
Journal: Nature neuroscience
In common: rpy2, NetworkX, Plotly, 5 other tools
[7] doi:10.1371/journal.pbio.3003856 [code]
Aging and metabolism contribute separately to brain-body health.
Journal: PLoS biology
In common: rpy2, statsmodels, scikit-learn, 3 other tools, 2 references
[8] doi:10.3389/fnsys.2026.1822122 [code]
Convergence-divergence circuits for multimodal integration of innate and learned opponent valences.
Journal: Frontiers in systems neuroscience
In common: NetworkX, Plotly, scikit-learn, 3 other tools, 2 references
[9] doi:10.1016/j.isci.2026.116825 [code]
Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.
Journal: iScience
In common: Pingouin, NetworkX, Plotly, 5 other tools
[10] doi:10.1080/20002297.2026.2705667 [code]
Oral microbiota dysbiosis related to the cortical thinning and cognitive impairment in cerebral small vessel disease.
Journal: Journal of oral microbiology
In common: 5 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.