OSCR

Brainwaves under medication: revealing class-specific neural signatures of psychotropic medication from 24,000 EEGs.

Code ↔ Paper

8 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 8 matches
  1. [1] § Methods › Medicine groups ↔ Brainwaves_Under_Medication_Visualisation_and_PCA_analysis_code_clean.ipynb, lines 172–219 · score 0.83 · sedative hypnotic, NaSSA, AChE, Anticholinergic, opioid, SNRIs
  2. [2] § Methods › Preprocessing ↔ preprocessing_REST_ASR_flexible_commented.m, lines 64–200 · score 0.80 · high pass filter, window criterion, channel interpolation, ASR, pipeline, FASTER
  3. [3] § Methods › Preprocessing ↔ preprocessing_REST_ASR_flexible_commented.m, lines 1–62 · score 0.71 · CleanLine, infinity, kurtosis, EEGLAB, plugin, noise
  4. [4] § Results › Data ↔ Brainwaves_Under_Medication_Visualisation_and_PCA_analysis_code_clean.ipynb, lines 172–219 · score 0.67 · AED Ca, AED Na, NaSSA, SARI, AP, atypical
  5. [5] § Methods › Dimensionality reduction ↔ Brainwaves_Under_Medication_code_commented.m, lines 253–282 · score 0.62 · confidence interval, Principal Component, uncorrelated, coefficients, variance, PCA
  6. [6] § Methods › EEG signal features ↔ Brainwaves_Under_Medication_code_commented.m, lines 37–58 · score 0.62 · feature exceeded, standard deviations, VAR, outliers, scored, patients
  7. [7] § Methods › Medicine groups ↔ balance_groups_meds_DN.m, lines 1–55 · score 0.60 · Chi squared, classified, medication classes, binary, psychotropic, diagnosis
  8. [8] § Results › Data ↔ Brainwaves_Under_Medication_Visualisation_and_PCA_analysis_code_clean.ipynb, lines 938–1070 · score 0.56 · hierarchical regression model, Holm, mixed, dimensionality, PCA, matched

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

  1. # %% [markdown]
  2. # # Brainwaves Under Medication: visualisation and PCA analysis
  3. #
  4. # This notebook reads the outputs generated by the accompanying MATLAB analysis script and produces:
  5. # - summary tables for significant PCA components,
  6. # - overview plots showing the variance explained by significant PCs,
  7. # - example PCA-loading plots,
  8. # - visualisations of high-loading features for medication-related PCs.
  9. #
  10. # The code is organised so that project-specific inputs are concentrated in the setup cells below. Update the paths and, if needed, the feature-column mapping before running the notebook.
  11. # %% [markdown]
  12. # ## 1. Analysis settings
  13. # %%
  14. # Number of independent matched comparisons used in the MATLAB script.
  15. n_matching_repetitions = 10
  16. # A PCA component is treated as robustly significant only if it is significant
  17. # in at least this many matched comparisons.
  18. min_significant_matches = 7
  19. # PCA variance threshold used in the MATLAB script. The notebook uses this
  20. # threshold when summarising/loadings PCs from the PCA decomposition.
  21. min_variance_explained_percent = 90
  22. # Significance threshold after Holm-Bonferroni correction.
  23. alpha = 0.05
  24. # %% [markdown]
  25. # ## 2. Imports and helper functions
  26. # %%
  27. import os
  28. import warnings
  29. import h5py
  30. import mne
  31. import numpy as np
  32. import pandas as pd
  33. import scipy.io
  34. import seaborn as sns
  35. import statsmodels.formula.api as smf
  36. from statsmodels.stats.multitest import multipletests
  37. import matplotlib
  38. import matplotlib.pyplot as plt
  39. import matplotlib.gridspec as gridspec
  40. from matplotlib import cm
  41. from matplotlib.colors import Normalize
  42. from mne.channels.layout import _find_topomap_coords
  43. %matplotlib inline
  44. warnings.filterwarnings("ignore")
  45. # %% [markdown]
  46. # ## 3. Define paths
  47. # %%
  48. # Folder containing MATLAB outputs from the published MATLAB script, EEG features and patient data.
  49. # Expected files include:
  50. # cleaned_data/data_new.h5
  51. # cleaned_data/PCA_data.mat
  52. # matched_indices.mat
  53. # Effects_medicines_PCA_OvR.mat
  54. # Effects_medicines_PCA_DN.mat
  55. matlab_output_directory = r'...'
  56. # CSV file describing each EEG feature/column in the EEG feature matrix.
  57. feature_metadata_file = r'...'
  58. # An example EEGLAB .set file. It is used only to read channel locations for
  59. # topographic plots. The participant does not need to be part of the analysis.
  60. example_eeg_file = r'...'
  61. # Output folder for figures generated by this notebook.
  62. figure_directory = os.path.join(matlab_output_directory, "graphs")
  63. if not os.path.isdir(figure_directory):
  64. os.mkdir(figure_directory)
  65. # %% [markdown]
  66. # ## 4. Load cleaned EEG data and metadata
  67. # %%
  68. cleaned_data_file = os.path.join(matlab_output_directory, "cleaned_data", "data_new.h5")
  69. with h5py.File(cleaned_data_file, "r") as h5_file:
  70. data_meds = np.transpose(np.array(h5_file["data_meds"]))
  71. zdata = np.transpose(np.array(h5_file["zdata"]))
  72. data = np.transpose(np.array(h5_file["data"]))
  73. metadata = np.transpose(np.array(h5_file["metadata"]))
  74. print("Medication matrix:", data_meds.shape) #N participants x N medicines
  75. print("Z-scored EEG feature matrix:", zdata.shape) #N participants x N features
  76. print("Participant metadata:", metadata.shape) #N participants x 5
  77. # metadata columns from the MATLAB script:
  78. # column 0 = sex
  79. # column 1 = age
  80. # column 2 = diagnosis
  81. # column 3 = centred recording year
  82. # column 4 = recording site
  83. # %% [markdown]
  84. # ## 5. Load and standardise feature descriptions
  85. # %%
  86. feature_metadata = pd.read_csv(feature_metadata_file)
  87. # Standard column names expected by the rest of the notebook.
  88. standard_feature_columns = [
  89. "measures",
  90. "events",
  91. "channel1",
  92. "channel2",
  93. "freq",
  94. "feat_group",
  95. ]
  96. # Map each standard name to the corresponding column in your CSV file.
  97. # If your file already uses the standard names, leave this dictionary as is.
  98. # Example for a different CSV:
  99. # feature_column_map = {
  100. # "measures": "measure_name",
  101. # "events": "condition",
  102. # "channel1": "electrode_a",
  103. # "channel2": "electrode_b",
  104. # "freq": "frequency_hz",
  105. # "feat_group": "feature_type",
  106. # }
  107. feature_column_map = {
  108. "measures": "measures",
  109. "events": "events",
  110. "channel1": "channel1",
  111. "channel2": "channel2",
  112. "freq": "freq",
  113. "feat_group": "feat_group",
  114. }
  115. missing_columns = [
  116. source_column
  117. for source_column in feature_column_map.values()
  118. if source_column not in feature_metadata.columns
  119. ]
  120. if missing_columns:
  121. raise ValueError(
  122. "The following columns are missing from feature_metadata_file: "
  123. + ", ".join(missing_columns)
  124. )
  125. # Rename user-specified columns to the standard names used below.
  126. rename_to_standard = {
  127. source_column: standard_column
  128. for standard_column, source_column in feature_column_map.items()
  129. }
  130. feature_metadata = feature_metadata.rename(columns=rename_to_standard)
  131. # Keep the standard columns first and preserve any additional columns after them.
  132. extra_columns = [
  133. column for column in feature_metadata.columns
  134. if column not in standard_feature_columns
  135. ]
  136. feature_metadata = feature_metadata[standard_feature_columns + extra_columns].copy()
  137. # Make sure frequency values are numeric where possible.
  138. feature_metadata["freq"] = pd.to_numeric(feature_metadata["freq"], errors="coerce")
  139. feature_metadata.head()
  140. # %%
  141. # Basic labels used throughout the notebook.
  142. event_names = np.unique(feature_metadata["events"].values)
  143. channel_names = ['Fp1', 'Fp2', 'F7', 'F3', 'Fz', 'F4', 'F8', 'T3','C3', 'Cz', 'C4', 'T4', 'T5', 'P3', 'Pz', 'P4', 'T6', 'O1', 'O2']
  144. measure_names = np.unique(feature_metadata["measures"].values)
  145. frequency_grid = np.arange(4, 60.5, 0.5)
  146. # Medication names must be in the same order as the columns of data_meds.
  147. medication_names = [
  148. "Anticholinergic",
  149. "BDZ",
  150. "NaSSA",
  151. "SARI",
  152. "SNRIs",
  153. "SSRIs",
  154. "AChEi",
  155. "AP atypical",
  156. "opioid",
  157. "sedative-hypnotic",
  158. "TCA",
  159. "AP typical",
  160. "AED_Na",
  161. "AED_Ca"
  162. ]
  163. # Short labels used in figure axes where long names would be difficult to read.
  164. medication_short_names = [
  165. "A-Ach",
  166. "BDZ",
  167. "NaSSA",
  168. "SARI",
  169. "SNRIs",
  170. "SSRIs",
  171. "AChEi",
  172. "AP atyp",
  173. "opioid",
  174. "hypno",
  175. "TCA",
  176. "AP typ",
  177. "AED (Na)",
  178. "AED (Ca)"
  179. ]
  180. if data_meds.shape[1] != len(medication_names):
  181. raise ValueError(
  182. "The number of medication names does not match the number of columns "
  183. "in data_meds."
  184. )
  185. # %% [markdown]
  186. # ## 6. Load MATLAB outputs
  187. # %%
  188. # Matched participant indices generated by the MATLAB script.
  189. matched_indices_file = scipy.io.loadmat(
  190. os.path.join(matlab_output_directory, "matched_indices.mat"),
  191. squeeze_me=True,
  192. )
  193. matched_indices_ovr = matched_indices_file["matched_indices_OvR"]
  194. matched_indices_drug_naive = matched_indices_file["matched_indices_DN"]
  195. # Effect-size and p-value statistics generated by the MATLAB script.
  196. effects_ovr = scipy.io.loadmat(
  197. os.path.join(matlab_output_directory, "Effects_medicines_PCA_OvR.mat"),
  198. squeeze_me=True,
  199. )
  200. effects_drug_naive = scipy.io.loadmat(
  201. os.path.join(matlab_output_directory, "Effects_medicines_PCA_DN.mat"),
  202. squeeze_me=True,
  203. )
  204. # PCA decomposition generated by the MATLAB script.
  205. pca_file = os.path.join(matlab_output_directory, "cleaned_data", "PCA_data.mat")
  206. with h5py.File(pca_file, "r") as h5_file:
  207. pca_coefficients = np.array(h5_file["PCA_coeff"])
  208. pca_scores = np.array(h5_file["PCA_score"])
  209. pca_explained_variance = np.asarray(h5_file["explained"]).ravel()
  210. print("PCA scores:", pca_scores.shape)
  211. print("PCA coefficients:", pca_coefficients.shape)
  212. print("Explained variance vector:", pca_explained_variance.shape)
  213. # %%
  214. np.shape(matched_indices_drug_naive[8][7])
  215. # %%
  216. # Optional check: print how many matched pairs are available for each medication
  217. # in the first matching repetition of the OvR comparison.
  218. for medication_index, medication_name in enumerate(medication_names):
  219. n_matched_pairs = len(matched_indices_ovr[medication_index][0])
  220. print(medication_name, n_matched_pairs)
  221. # %% [markdown]
  222. # ## 7. Load channel locations for topographic plots
  223. # %%
  224. # MNE uses channel locations stored in example_raw_eeg.info to draw scalp maps.
  225. # The standard_1020 montage is applied so the plots use standard EEG positions.
  226. example_raw_eeg = mne.io.read_raw_eeglab(example_eeg_file, preload=False)
  227. montage_1020 = mne.channels.make_standard_montage("standard_1020")
  228. example_raw_eeg.set_montage(montage_1020)
  229. # Keep only channels that are present both in the feature metadata and in the
  230. # example EEG file. This avoids plotting errors if the feature table contains
  231. # channels absent from the example file.
  232. channel_names = np.array([
  233. channel for channel in channel_names
  234. if channel in example_raw_eeg.ch_names
  235. ])
  236. sensor_adjacency, sensor_names = mne.channels.find_ch_adjacency(
  237. example_raw_eeg.info,
  238. ch_type="eeg",
  239. )
  240. print("Channels available for plotting:", len(channel_names))
  241. # %% [markdown]
  242. # ## 8. Calculate mean effect sizes and robustly significant PCs
  243. # %%
  244. mean_hedges_g_ovr = []
  245. mean_hedges_g_drug_naive = []
  246. significant_pc_indices_ovr = []
  247. significant_pc_indices_drug_naive = []
  248. for medication_index in range(len(medication_names)):
  249. # Average Hedges' g across repeated matched samples.
  250. # Rows = PCA components; columns = matching repetitions.
  251. hedges_g_ovr = np.asarray(effects_ovr["Hedges_g_all"][medication_index], dtype=float)
  252. hedges_g_drug_naive = np.asarray(effects_drug_naive["Hedges_g_all"][medication_index], dtype=float)
  253. mean_hedges_g_ovr.append(np.mean(hedges_g_ovr, axis=1))
  254. mean_hedges_g_drug_naive.append(np.mean(hedges_g_drug_naive, axis=1))
  255. # Identify PCs that are significant in at least min_significant_matches independent matched comparisons.
  256. corrected_p_ovr = np.asarray(np.stack(effects_ovr["P_corr"][medication_index]), dtype=float)
  257. corrected_p_drug_naive = np.asarray(np.stack(effects_drug_naive["P_corr"][medication_index]), dtype=float)
  258. significant_pc_indices_ovr.append(
  259. np.where(np.sum(corrected_p_ovr < alpha, axis=0) >= min_significant_matches)[0]
  260. )
  261. significant_pc_indices_drug_naive.append(
  262. np.where(np.sum(corrected_p_drug_naive < alpha, axis=0) >= min_significant_matches)[0]
  263. )
  264. significant_pc_indices_ovr
  265. # %%
  266. np.shape(mean_hedges_g_ovr), np.shape(significant_pc_indices_ovr)
  267. # %% [markdown]
  268. # ### Show effect size and p-value for significant components (supplementary tables)
  269. # %%
  270. significant_effect_tables = {}
  271. for medication_index, medication_name in enumerate(medication_names):
  272. significant_pcs = significant_pc_indices_ovr[medication_index]
  273. best_pcs = pd.DataFrame([], columns=['PC', 'var explained', 'p_OvR', 'g_OvR', 'g_CI_OvR','nmatch_OvR',
  274. 'p_DN', 'g_DN', 'Hg_CI_DN', 'nmatch_DN'])
  275. #PC number, explained variance,
  276. #corrected p, Hedges' g, 95% confidence intervals for Hedges' g, number of matches where component was significant
  277. #in OvR comparison and in drug-naive comparison
  278. if len(significant_pcs) > 0:
  279. corrected_p_ovr = np.asarray(np.stack(effects_ovr["P_corr"][medication_index]), dtype=float)
  280. corrected_p_dn = np.asarray(np.stack(effects_drug_naive["P_corr"][medication_index]), dtype=float)
  281. for pc in significant_pcs:
  282. #calculate median p-value
  283. p1 = np.median(corrected_p_ovr[:, pc])
  284. p2 = np.median(corrected_p_dn[:, pc])
  285. #calculate in how many comparisons the effect was significant
  286. nm1 = np.sum(corrected_p_ovr[:, pc] < 0.05, 0)
  287. nm2 = np.sum(corrected_p_dn[:, pc] < 0.05, 0)
  288. #mean effects and their CIs
  289. hg1 = abs(mean_hedges_g_ovr[medication_index][pc])
  290. ci_u = abs(np.mean(np.stack(effects_ovr['CI_upp_all'][medication_index]), 1)[pc])
  291. ci_l = abs(np.mean(np.stack(effects_ovr['CI_low_all'][medication_index]), 1)[pc])
  292. cis_ovr = '[' + str(round(min(ci_l, ci_u), 2)) + ',' + str(round(max(ci_l, ci_u), 2)) + ']'
  293. hg2 = abs(mean_hedges_g_drug_naive[medication_index][pc])
  294. ci_u = abs(np.mean(np.stack(effects_drug_naive['CI_upp_all'][medication_index]), 1)[pc])
  295. ci_l = abs(np.mean(np.stack(effects_drug_naive['CI_low_all'][medication_index]), 1)[pc])
  296. cis_dn = '[' + str(round(min(ci_l, ci_u), 2)) + ',' + str(round(max(ci_l, ci_u), 2)) + ']'
  297. best_pcs.loc[len(best_pcs)] = [pc+1, round(pca_explained_variance[pc], 2),
  298. round(p1, 3), round(hg1, 2), cis_ovr, nm1,
  299. round(p2, 3), round(hg2, 2), cis_dn, nm2]
  300. #sort by OvR effect size
  301. best_pcs.sort_values('g_OvR', ascending = False, inplace=True)
  302. significant_effect_tables[medication_name] = best_pcs
  303. # %%
  304. # Example table. Change "SSRIs" to any medication name from medication_names.
  305. significant_effect_tables["SSRIs"]
  306. # %% [markdown]
  307. # ## 9. Visualization
  308. # %%
  309. # Choose which comparison to summarise in subsequent figures.
  310. # Options:
  311. # "OvR" = medication group versus other medicated participants
  312. # "DN" = medication group versus drug-naive participants
  313. comparison = "OvR"
  314. # %% [markdown]
  315. # ### 9.1 total variance explained by significant PCs
  316. # %%
  317. if comparison == "OvR":
  318. significant_pcs = significant_pc_indices_ovr
  319. elif comparison == "DN":
  320. significant_pcs = significant_pc_indices_drug_naive
  321. else:
  322. raise ValueError("comparison_for_variance_plot must be 'OvR' or 'DN'.")
  323. expl_var_meds = np.zeros(len(significant_pcs))
  324. for medication_index in range(len(significant_pcs)):
  325. if len(significant_pcs[medication_index]) > 0:
  326. expl_var_meds[medication_index] = np.sum(pca_explained_variance[significant_pcs[medication_index]])
  327. #expl_var_meds = expl_var_meds.flatten()
  328. meds_signif = np.array(medication_names)[expl_var_meds > 0]
  329. expl_signif = expl_var_meds[expl_var_meds > 0]
  330. print(meds_signif)
  331. print(expl_signif)
  332. # %%
  333. fig, ax = plt.subplots(figsize=(8, 6))
  334. sns.barplot(
  335. x=meds_signif,
  336. y=expl_signif,
  337. palette="tab10",
  338. ax=ax,
  339. )
  340. ax.set_ylabel("Variance explained by \n significant PCs (%)", size=18)
  341. ax.set_xlabel("Medication group", size=18)
  342. ax.tick_params(axis="y", labelsize=16)
  343. ax.set_xticklabels(
  344. meds_signif,
  345. size=16,
  346. rotation=30,
  347. ha="right",
  348. )
  349. plt.tight_layout()
  350. variance_plot_png = os.path.join(
  351. figure_directory,
  352. "explained_variance_significant_pcs.png",
  353. )
  354. variance_plot_svg = os.path.join(
  355. figure_directory,
  356. "explained_variance_significant_pcs.svg",
  357. )
  358. plt.savefig(variance_plot_png, dpi=900)
  359. plt.savefig(variance_plot_svg, dpi=900, format="svg")
  360. plt.show()
  361. # %% [markdown]
  362. # ### 9.2 Identify high-loading features for each PCA component
  363. # %%
  364. # Count how many features belong to each feature group in the full dataset.
  365. fgs, fgcnts = np.unique(feature_metadata['feat_group'].values, return_counts=True)
  366. fgs, fgcnts
  367. # %%
  368. # Feature groups shown in summary bar plots.
  369. # The first entry combines frequency-binned and frequency-resolved coherence
  370. # features into one broader "coherence" category.
  371. best_feats_allPC = {}
  372. props_allPC = []
  373. nbpca = np.where(np.cumsum(pca_explained_variance) > min_variance_explained_percent)[0][0]
  374. for pc in range(0,nbpca):
  375. best_feats_allPC[pc] = {}
  376. feats_comp = pca_coefficients[pc, :]
  377. #ADD feature components to each PC
  378. feature_metadata_by_pc = feature_metadata.copy()
  379. feature_metadata_by_pc['feature_eigenvalues'] = feats_comp
  380. #calculate mean and standard deviation for absolsute PC coefficients and find features that have coefficients higher than 2sd above the mean
  381. mm = np.mean(abs(feats_comp))
  382. sd = np.std(abs(feats_comp))
  383. best_feats = abs(feats_comp)>mm+2*sd
  384. #change indices to feature descriptions
  385. best_feats_allPC[pc] = feature_metadata_by_pc.iloc[best_feats, :]
  386. #calculate the contribution of each feature group in this PCA relative to the contribution of this group in whole data
  387. pca_fgs, pca_fgcnts = np.unique(best_feats_allPC[pc]['feat_group'].values, return_counts=True)
  388. props_l_pca = [0,0,0,0,0]
  389. for fi in range(0, len(pca_fgcnts)):
  390. fi2 = list(fgs).index(pca_fgs[fi])
  391. prop = pca_fgcnts[fi]/fgcnts[fi2]
  392. props_l_pca[fi2] = prop
  393. props_allPC.append(props_l_pca)
  394. # %% [markdown]
  395. # ### 9.3 Plot example PCA components
  396. # %%
  397. comps_to_plot = 10 #how many components to plot (integer between 1 and nbpca)
  398. example_pca_directory = os.path.join(figure_directory, "example_PCA_components")
  399. if not os.path.isdir(example_pca_directory):
  400. os.makedirs(example_pca_directory)
  401. # Colorbar placement within topomap axes.
  402. colorbar_x_start = 1.03
  403. colorbar_x_width = 0.04
  404. colorbar_y_start = 0.05
  405. colorbar_y_height = 0.90
  406. for pc in range(comps_to_plot):
  407. feats_pca = best_feats_allPC[pc]
  408. fig, ax = plt.subplots(1,3, figsize=(18, 5), width_ratios=[1.1,1.1,0.8])
  409. #FREQUENCY
  410. freq_in_range_oi = []
  411. for fqi, fq in zip(feats_pca.index.values, feats_pca['freq'].values):
  412. if fq in frequency_grid:
  413. freq_in_range_oi.append(fqi)
  414. feats_pca1 = feats_pca.copy()
  415. feats_pca1 = feats_pca1.loc[freq_in_range_oi, :]
  416. freq_values, freq_counts = np.unique(feats_pca1['freq'].astype(float).values, return_counts = True)
  417. #frequency plot
  418. ax1 = ax[1]
  419. ax1.set_xlim(0, 61)
  420. ax1.bar(freq_values, freq_counts, width=0.5, color='navy')
  421. ax1.set_xticks(range(0,61,10))
  422. ax1.tick_params(axis='both', labelsize=18)
  423. ax1.set_xlabel("Frequency [Hz]", fontsize=20)
  424. ax1.set_ylabel('N features \n (eigenvalue > M+2SD)', fontsize=20)
  425. ax1.text(0.5, 1.02, "Frequencies", horizontalalignment='center', verticalalignment='bottom', size=20, transform=ax1.transAxes)
  426. #CHANNELS
  427. feats_pca2 = feats_pca.copy()
  428. feats_pca2['channel1'] = feats_pca['channel2']
  429. feats_pca2 = feats_pca2.append(feats_pca)
  430. names_ch, counts_ch = np.unique(feats_pca2['channel1'], return_counts = True)
  431. counts_ch2 = np.zeros((np.shape(channel_names)))
  432. for chi, ch in enumerate(channel_names):
  433. if ch in names_ch:
  434. counts_ch2[chi] = counts_ch[np.where(names_ch==ch)]
  435. #channels topomap plot
  436. ax2 = ax[2]
  437. im, cm2 = mne.viz.plot_topomap(counts_ch2, example_raw_eeg.info, names=channel_names,
  438. cmap = 'inferno', axes=ax2, show=False)
  439. #increase fontsize for channel names
  440. for tt in plt.findobj(fig, matplotlib.text.Text):
  441. if tt.get_text() in example_raw_eeg.ch_names:
  442. tt.set_fontsize(16)
  443. #colorbar
  444. cbar_ax = ax2.inset_axes([colorbar_x_start, colorbar_y_start, colorbar_x_width, colorbar_y_height])
  445. clb = fig.colorbar(im, cax=cbar_ax)
  446. clb.set_label("N features \n (eigenvalue > M+2SD)", size=18)
  447. clb.ax.tick_params(labelsize=16)
  448. ax2.text(0.5, 1.02, "Channels", horizontalalignment='center', verticalalignment='bottom', size=20, transform=ax2.transAxes)
  449. #feature groups
  450. ax3 = ax[0]
  451. fg_names = ['coherence', 'phase \n synchrony', 'spectral', 'nonlinear']
  452. fg_counts = [np.mean([props_allPC[pc][0]*100, props_allPC[pc][1]*100]), props_allPC[pc][2]*100, props_allPC[pc][3]*100, props_allPC[pc][4]*100]
  453. ax3.barh(fg_names, fg_counts, color='darkgreen', edgecolor = 'white')
  454. ax3.tick_params(axis='both', labelsize=20)
  455. ax3.set_xlabel("% of all features \n (eigenvalue > M+2SD)", fontsize=20)
  456. ax3.text(0.5, 1.02, "Feature types", horizontalalignment='center', verticalalignment='bottom', size=20, transform=ax3.transAxes)
  457. evar = round(pca_explained_variance[pc], 2)
  458. fig.suptitle('PCA' + str(pc+1) + ' (explained variance ' + str(evar) + '%)', fontsize=22, fontweight='bold')
  459. plt.tight_layout()
  460. plt.savefig(os.path.join(example_pca_directory, 'PCA' + str(pc+1) + '.svg'), dpi=900, format="svg")
  461. plt.savefig(os.path.join(example_pca_directory, 'PCA' + str(pc+1) + '.png'), dpi=900)
  462. plt.close()
  463. #Note: in graph titles and filenames, PC's are labeled from 1, not from 0
  464. # %% [markdown]
  465. # ### 9.4 Plot high-loading features for significant medication-related PCs
  466. # %%
  467. #Function for connectivity plots
  468. from matplotlib import cm
  469. from matplotlib.colors import Normalize
  470. from mne.channels.layout import _find_topomap_coords
  471. def plot_sensor_connectivity_topomap(
  472. info, connectivity_matrix, top_n=30,
  473. v_min=0.015, v_max=0.050, cmap='viridis',
  474. ax=None, save_path=None, cbar_label="z"):
  475. """
  476. Plot 2D EEG sensor connectivity on top of sensor topomap.
  477. Parameters
  478. ----------
  479. info : instance of mne.Info
  480. EEG channel info.
  481. connectivity_matrix : ndarray, shape (n_channels, n_channels)
  482. Symmetric matrix of connectivity values between EEG channels.
  483. top_n : int
  484. Number of top connections to display.
  485. v_min, v_max : float
  486. Color scale limits for connectivity strength.
  487. cmap : str
  488. Colormap to use.
  489. ax : matplotlib Axes | None
  490. If provided, plot into this axis (useful for subplots).
  491. If None, a new figure is created.
  492. save_path : str or None
  493. If provided, path to save the figure.
  494. """
  495. # Pick EEG sensors and get projected 2D coords
  496. picks = mne.pick_types(info, meg=False, eeg=True)
  497. n_channels = len(picks)
  498. assert connectivity_matrix.shape == (n_channels, n_channels), \
  499. f"Expected matrix of shape ({n_channels}, {n_channels})"
  500. coords = _find_topomap_coords(info, picks)
  501. # Extract top-N strongest connections
  502. triu_inds = np.triu_indices(n_channels, k=1)
  503. conn_vals = connectivity_matrix[triu_inds]
  504. top_idx = np.argsort(abs(conn_vals))[-top_n:]
  505. i1, i2 = triu_inds[0][top_idx], triu_inds[1][top_idx]
  506. top_vals = conn_vals[top_idx]
  507. # Create axis if not provided
  508. own_fig = False
  509. if ax is None:
  510. fig, ax = plt.subplots(figsize=(5.5, 5))
  511. own_fig = True
  512. else:
  513. fig = ax.figure
  514. # Plot EEG sensor positions
  515. mne.viz.plot_sensors(info, kind='topomap', show=False, axes=ax)
  516. # Normalize colors
  517. norm = Normalize(vmin=v_min, vmax=v_max)
  518. cmap_func = cm.ScalarMappable(norm=norm, cmap=cmap)
  519. # Draw connectivity lines
  520. for idx_a, idx_b, val in zip(i1, i2, top_vals):
  521. x1, y1 = coords[idx_a]
  522. x2, y2 = coords[idx_b]
  523. ax.plot([x1, x2], [y1, y2], color=cmap_func.to_rgba(val), linewidth=2)
  524. # Colorbar
  525. #ticksl = np.round(np.arange(v_min, v_max, 0.001), 3)
  526. #ticksl = np.arange(v_min, v_max)
  527. #cbar = fig.colorbar(cmap_func, ax=ax, fraction=0.046, pad=0.03, ticks=ticksl)
  528. cbar = fig.colorbar(cmap_func, ax=ax, fraction=0.047, pad=0.02, shrink=0.9) #, ticks=ticksl)
  529. #cbar.set_label('Mean eigenvalue', size=18)
  530. cbar.set_label(cbar_label, size=20, labelpad=-50, y=1.05) #, rotation=90) #rotation??
  531. cbar.ax.tick_params(labelsize=16)
  532. # Save or show
  533. if save_path:
  534. if save_path.endswith('.svg'):
  535. fig.savefig(save_path, dpi=900, bbox_inches='tight', format='svg')
  536. else:
  537. fig.savefig(save_path, dpi=900, bbox_inches='tight')
  538. print(f"Saved plot to {save_path}")
  539. elif own_fig:
  540. plt.show()
  541. if own_fig:
  542. plt.close(fig)
  543. # %%
  544. if comparison == "OvR":
  545. signif_pcs = significant_pc_indices_ovr
  546. mean_effsizes = mean_hedges_g_ovr
  547. comparison_label = "others"
  548. elif comparison == "DN":
  549. signif_pcs = significant_pc_indices_drug_naive
  550. mean_effsizes = mean_hedges_g_drug_naive
  551. comparison_label = "drug-naive"
  552. else:
  553. raise ValueError("comparison_for_pc_plots must be 'OvR' or 'DN'.")
  554. significant_pc_figure_directory = os.path.join(
  555. figure_directory,
  556. "significant_PCs_{0}".format(comparison),
  557. )
  558. if not os.path.isdir(significant_pc_figure_directory):
  559. os.makedirs(significant_pc_figure_directory)
  560. plot_panel_titles = [
  561. "Types of features",
  562. "Coherence frequencies",
  563. "PSD frequencies",
  564. "Nonlinear channels",
  565. "Coherence connections",
  566. "PSD channels",
  567. ]
  568. subplot_labels = ["A", "B", "C", "D", "E", "F"]
  569. #feature_group_plot_labels = [label for label, _ in plot_feature_group_order]
  570. top_channels_psd_by_medication = {}
  571. top_channels_nonlinear_by_medication = {}
  572. # Measures whose sign is reversed to match the interpretation used in the manuscript (higher values mean greater complexity).
  573. #Edit this list if the feature definitions change.
  574. nonlinear_measures_with_reversed_direction = ["Hjorth_complexity", "DFA_"]
  575. # %%
  576. for medication_index, medication_name in enumerate(medication_names):
  577. top_channels_psd_by_medication[medication_index] = []
  578. top_channels_nonlinear_by_medication[medication_index] = []
  579. if not os.path.isdir(os.path.join(significant_pc_figure_directory, medication_name)):
  580. os.mkdir(os.path.join(significant_pc_figure_directory, medication_name))
  581. for pc in signif_pcs[medication_index]:
  582. high_loading_features = best_feats_allPC[pc]
  583. # Orient PCA loadings so that positive values correspond to larger
  584. # values in the medication group relative to the comparison group.
  585. pca_sign = 1
  586. if mean_effsizes[medication_index][pc] < 0:
  587. pca_sign = -1
  588. high_loading_features['feature_eigenvalues'] = high_loading_features['feature_eigenvalues']*pca_sign
  589. #reverse the sign for complexity features for which higher values mean less complexity
  590. reverse_measure_mask = high_loading_features["measures"].isin(nonlinear_measures_with_reversed_direction)
  591. high_loading_features.loc[reverse_measure_mask, 'feature_eigenvalues'] = -high_loading_features.loc[reverse_measure_mask, 'feature_eigenvalues'].values
  592. #FIGURE
  593. fig = plt.figure(figsize=(20, 12)) #, constrained_layout=True)
  594. outer = gridspec.GridSpec(2, 3, wspace=0.3, hspace=0.3, height_ratios = [3,2])
  595. conn_temptate = np.zeros((19,19))
  596. for mg, sbi in zip(['coh_fq', 'linear_psd'], [1,2]):
  597. df_mg = high_loading_features.loc[high_loading_features['feat_group']==mg, :]
  598. df_mg['abs_pcacoef'] = abs(df_mg['feature_eigenvalues'])
  599. df_mg_p = df_mg.loc[df_mg['feature_eigenvalues'] > 0, :]
  600. df_mg_m = df_mg.loc[df_mg['feature_eigenvalues'] < 0, :]
  601. #best channels
  602. channels_mean_coeff = df_mg.groupby(['channel1']).mean('abs_pcacoef').sort_values(by = ['abs_pcacoef'], ascending=False)
  603. top_channels_psd_by_medication[medication_index].append(channels_mean_coeff.index.values[0:5])
  604. # ---------------Frequency plots (for psd and connectivity)------------------------------
  605. nmf_p, cntf_p = np.unique(df_mg_p['freq'].astype(float).values, return_counts = True)
  606. nmf_m, cntf_m = np.unique(df_mg_m['freq'].astype(float).values, return_counts = True)
  607. inner = gridspec.GridSpecFromSubplotSpec(2, 1, subplot_spec=outer[sbi], wspace=0, hspace=0.2)
  608. ax1 = plt.Subplot(fig, inner[0])
  609. ax2 = plt.Subplot(fig, inner[1])
  610. #plot frequencies where medicine group has higher values than the comparison group
  611. #in the positive axis and red color and where it has lower values in negative axis and blue color
  612. ax1.bar(nmf_p, cntf_p, width=0.5, color='indianred')
  613. ax2.bar(nmf_m, cntf_m, width=0.5, color='royalblue')
  614. ax1.set_xlim(0, 61)
  615. ax1.set_xticks(range(0,61,10))
  616. ax1.set_xticklabels([])
  617. ax1.tick_params(axis='y', labelsize=18, direction='out', length=5)
  618. ax1.set_ylabel(medication_short_names[medication_index] + ' > ' + comparison_label, fontsize=19, loc='center')
  619. ax2.set_xlim(0, 61)
  620. ax2.set_xticks(range(0,61,10))
  621. ax2.set_xlabel("Frequency [Hz]", fontsize=20, loc='center')
  622. ax2.set_ylabel(medication_short_names[medication_index] + ' < ' + comparison_label, fontsize=19, loc='center')
  623. #inverted joined y axis
  624. ax2.invert_yaxis()
  625. ax2.tick_params(axis='both', labelsize=18, direction='out', length=5)
  626. ax2.xaxis.tick_top()
  627. #ensure the same limits for both positive and negative axes
  628. lim = max(max(ax1.get_ylim()), max(ax2.get_ylim()))
  629. ax1.set_ylim(0, lim)
  630. ax2.set_ylim(lim, 0)
  631. ax1.text(0.5, 1.02, plot_panel_titles[sbi], horizontalalignment='center', verticalalignment='bottom', size=20, transform=ax1.transAxes)
  632. ax1.text(-0.1, 1.02, subplot_labels[sbi], weight = 'bold', horizontalalignment='right', verticalalignment='bottom', size=22, transform=ax1.transAxes)
  633. ax1.text(-0.01, 1.00, 'N', horizontalalignment='right', verticalalignment='bottom', size=18, transform=ax1.transAxes)
  634. #move the x-axis label to the center and remove the double zero label
  635. yticks2 = ax2.yaxis.get_major_ticks()
  636. yticks2[0].set_visible(False)
  637. fig.add_subplot(ax1)
  638. fig.add_subplot(ax2)
  639. # ---------------Sensor topomap plots (for psd and connectivity)----------------------------
  640. inner = gridspec.GridSpecFromSubplotSpec(1, 1, subplot_spec=outer[sbi+3], wspace=0, hspace=0) #, width_ratios=[5, 1])
  641. ax = plt.Subplot(fig, inner[0])
  642. cnt_ch = np.zeros((len(channel_names), ))
  643. nm_ch1, cnt_ch1 = np.unique(df_mg['channel1'], return_counts = True)
  644. nm_ch2, cnt_ch2 = np.unique(df_mg['channel2'], return_counts = True)
  645. nm_ch1 = list(nm_ch1)
  646. nm_ch2 = list(nm_ch2)
  647. cnt_ch1 = list(cnt_ch1)
  648. cnt_ch2 = list(cnt_ch2)
  649. for chi in range(0, len(channel_names)):
  650. if channel_names[chi] not in nm_ch1:
  651. nm_ch1.append(channel_names[chi])
  652. cnt_ch1.append(0)
  653. if channel_names[chi] not in nm_ch2:
  654. nm_ch2.append(channel_names[chi])
  655. cnt_ch2.append(0)
  656. cnt_ch[chi] = cnt_ch1[nm_ch1.index(channel_names[chi])] + cnt_ch2[nm_ch2.index(channel_names[chi])]
  657. if mg == 'linear_psd':
  658. im, cm2 = mne.viz.plot_topomap(cnt_ch, example_raw_eeg.info, names=channel_names,
  659. cmap = 'Blues', axes=ax, show=False)
  660. #colorbar
  661. cbar_ax = ax.inset_axes([colorbar_x_start, colorbar_y_start, colorbar_x_width, colorbar_y_height])
  662. clb = fig.colorbar(im, cax=cbar_ax)
  663. clb.ax.tick_params(labelsize=16)
  664. cbar_ax.text(0, 1.02, 'N', horizontalalignment='left', verticalalignment='bottom', size=18, transform=cbar_ax.transAxes)
  665. elif mg == 'coh_fq':
  666. for chi1 in range(len(channel_names)):
  667. for chi2 in range(chi1, len(channel_names)):
  668. df_ch = df_mg.loc[df_mg['channel1'] == channel_names[chi1]].loc[df_mg['channel2'] == channel_names[chi2]]
  669. conn_temptate[chi1, chi2] = len(df_ch)
  670. conn_temptate[chi2, chi1] = len(df_ch)
  671. plot_sensor_connectivity_topomap(example_raw_eeg.info, conn_temptate, top_n=30,
  672. v_min = np.min(conn_temptate[conn_temptate > 0]), v_max=np.max(conn_temptate),
  673. cmap='Blues', ax=ax)
  674. ax.text(0.5, 1.02, plot_panel_titles[sbi+3], horizontalalignment='center', verticalalignment='bottom', size=20, transform=ax.transAxes)
  675. ax.text(-0.1, 1.02, subplot_labels[sbi+3], weight = 'bold', horizontalalignment='right', verticalalignment='bottom', size=22, transform=ax.transAxes)
  676. fig.add_subplot(ax)
  677. # -------------- feature groups --------------
  678. inner = gridspec.GridSpecFromSubplotSpec(1, 1,
  679. subplot_spec=outer[0], wspace=0, hspace=0)
  680. ax = plt.Subplot(fig, inner[0])
  681. cnt_fg = [np.mean([props_allPC[pc][0]*100, props_allPC[pc][1]*100]), props_allPC[pc][2]*100, props_allPC[pc][3]*100, props_allPC[pc][4]*100]
  682. ax.barh(fg_names, cnt_fg, color='navy', edgecolor = 'white')
  683. ax.tick_params(axis='both', labelsize=20)
  684. ax.set_xlabel('% of all features', fontsize=20)
  685. ax.text(0.5, 1.02, plot_panel_titles[0], horizontalalignment='center', verticalalignment='bottom', size=20, transform=ax.transAxes)
  686. ax.text(-0.1, 1.02, subplot_labels[0], weight = 'bold', horizontalalignment='right', verticalalignment='bottom', size=22, transform=ax.transAxes)
  687. fig.add_subplot(ax)
  688. # --------- Nonlinear features - topomap ------------
  689. inner = gridspec.GridSpecFromSubplotSpec(1, 1,
  690. subplot_spec=outer[3], wspace=0, hspace=0)
  691. ax3 = plt.Subplot(fig, inner[0])
  692. df_mg = high_loading_features.loc[high_loading_features['feat_group']=='nonlinear', :]
  693. df_mg['abs_pcacoef'] = abs(df_mg['feature_eigenvalues'])
  694. df_mg_p = df_mg.loc[df_mg['feature_eigenvalues'] > 0, :]
  695. df_mg_m = df_mg.loc[df_mg['feature_eigenvalues'] < 0, :]
  696. #best channels
  697. channels_mean_coeff = df_mg.groupby(['channel1']).mean('abs_pcacoef').sort_values(by = ['abs_pcacoef'], ascending=False)
  698. top_channels_nonlinear_by_medication[medication_index].append(channels_mean_coeff.index.values[0:5])
  699. cnt_ch = np.zeros((len(channel_names), ))
  700. nm_chp, cnt_chp = np.unique(df_mg_p['channel1'], return_counts = True)
  701. nm_chm, cnt_chm = np.unique(df_mg_m['channel1'], return_counts = True)
  702. for chi in range(0, len(channel_names)):
  703. cnt_ch[chi] = 0
  704. if channel_names[chi] in nm_chp:
  705. cnt_ch[chi] = cnt_ch[chi] + cnt_chp[np.where(nm_chp == channel_names[chi])[0][0]]
  706. if channel_names[chi] in nm_chm:
  707. cnt_ch[chi] = cnt_ch[chi] - cnt_chm[np.where(nm_chm == channel_names[chi])[0][0]]
  708. #topomap
  709. vmax = np.max([abs(np.nanmin(cnt_ch)), abs(np.nanmax(cnt_ch))])
  710. im, cm2 = mne.viz.plot_topomap(cnt_ch, example_raw_eeg.info, names=channel_names,
  711. vlim=(-vmax, vmax),
  712. cmap = 'seismic', axes=ax3, show=False)
  713. #colorbar
  714. cbar_ax = ax3.inset_axes([colorbar_x_start, colorbar_y_start, colorbar_x_width, colorbar_y_height])
  715. clb = fig.colorbar(im, cax=cbar_ax)
  716. clb.ax.tick_params(labelsize=16)
  717. cbar_ax.text(0, 1.02, 'N', horizontalalignment='left', verticalalignment='bottom', size=18, transform=cbar_ax.transAxes)
  718. ax3.text(0.5, 1.02, plot_panel_titles[3], horizontalalignment='center', verticalalignment='bottom', size=20, transform=ax3.transAxes)
  719. ax3.text(-0.02, 1.02, subplot_labels[3], weight = 'bold', horizontalalignment='right', verticalalignment='bottom', size=22, transform=ax3.transAxes)
  720. fig.add_subplot(ax3)
  721. #increase fontsize for channelnames
  722. for tt in plt.findobj(fig, matplotlib.text.Text):
  723. if tt.get_text() in channel_names:
  724. tt.set_fontsize(16)
  725. evar = round(pca_explained_variance[pc], 2)
  726. fig.suptitle('PC' + str(pc+1) + ' (explained variance ' + str(evar) + '%)', fontsize=22, fontweight='bold')
  727. plt.subplots_adjust(left=0.15, top = 0.91, hspace = 0.1)
  728. plt.savefig(os.path.join(significant_pc_figure_directory, medication_name , 'PCA' + str(pc+1) + '.svg'), dpi=900, format="svg")
  729. plt.savefig(os.path.join(significant_pc_figure_directory, medication_name , 'PCA' + str(pc+1) + '.png'), dpi=900)
  730. plt.show()
  731. plt.close()
  732. # %% [markdown]
  733. # ## 10. Single-feature graphs
  734. #
  735. # Plots to visualise differences between medication groups and comparison groups in single features.
  736. # As the chosen viualisations often involve mean features from the several electrodes, the effects are calculated here, instead of using matlab output.
  737. # %%
  738. #Function that calculates the required group values from zdata
  739. #input:
  740. #feat_idx - numeric indexes of features in the data. can be multidimensional, but channels (or other dimension to be averaged) schold be in the last axis
  741. #matched_idx - indexes of persons from given groups
  742. #transform_log - boolean, whether to calculate log10 of the data values, defaul-no
  743. #average_chennels - boolean, whether the result should be averaged over channels, default-yes
  744. def get_meanfeaturevals_and_sem(feat_idx, zdata, metadata, matched_idx_ovr, matched_idx_dn, transform_log=False, average_channels=True):
  745. psd_nodrug = []
  746. psd_drug = []
  747. psd_drug_naive = []
  748. semI_drug = []
  749. semI_nodrug = []
  750. semI_drug_naive = []
  751. if average_channels:
  752. #mean of feature over channels
  753. psd_meanchann = np.mean(zdata[:, feat_idx], axis=-1)
  754. else:
  755. psd_meanchann = zdata[:, feat_idx]
  756. if transform_log:
  757. #rescale the mean psd so it above 0, to be able to calculate log10 of it
  758. psd_meanchann = psd_meanchann + abs(np.min(psd_meanchann)) + 0.001
  759. #preloacte p matrices
  760. dim_feat = 1
  761. if len(np.shape(psd_meanchann)) > 1:
  762. dim_feat = np.shape(psd_meanchann)[1]
  763. p_ovr = np.zeros((len(matched_idx_ovr), dim_feat))
  764. p_dn = np.zeros((len(matched_idx_ovr), dim_feat))
  765. #Create feature matrix by adressing zdata
  766. for i in range(len(matched_idx_ovr)):
  767. feature_drug = psd_meanchann[matched_idx_ovr[i][:, 0] - 1] #-1 converts the matlab index to python
  768. feature_nodrug = psd_meanchann[matched_idx_ovr[i][:, 1] - 1]
  769. feature_drug_naive = psd_meanchann[matched_idx_dn[i][:, 1] - 1]
  770. if transform_log==1:
  771. feature_drug = np.log10(feature_drug)
  772. feature_nodrug = np.log10(feature_nodrug)
  773. feature_drug_naive = np.log10(feature_drug_naive)
  774. #Mean over participants
  775. psd_drug.append(np.mean(feature_drug, axis=0))
  776. psd_nodrug.append(np.mean(feature_nodrug, axis=0))
  777. psd_drug_naive.append(np.mean(feature_drug_naive, axis=0))
  778. #standard error of the mean for the matrices
  779. semI_drug.append(scipy.stats.sem(feature_drug))
  780. semI_nodrug.append(scipy.stats.sem(feature_nodrug))
  781. semI_drug_naive.append(scipy.stats.sem(feature_drug_naive))
  782. # --------------- Significance for the mean of channels:
  783. #Create dataframe for hierarchical regression model for each frequency
  784. df_drug = pd.DataFrame(feature_drug)
  785. df_drug["year_c"] = metadata[matched_idx_ovr[i][:, 0] - 1, 3]
  786. df_drug["site"] = metadata[matched_idx_ovr[i][:, 0] - 1, 4]
  787. df_drug["group"] = "drugY"
  788. df_nodrug = pd.DataFrame(feature_nodrug)
  789. df_nodrug["year_c"] = metadata[matched_idx_ovr[i][:, 1] - 1, 3]
  790. df_nodrug["site"] = metadata[matched_idx_ovr[i][:, 1] - 1, 4]
  791. df_nodrug["group"] = "drugN"
  792. df_drugnai = pd.DataFrame(feature_drug_naive)
  793. df_drugnai["year_c"] = metadata[matched_idx_dn[i][:, 1] - 1, 3]
  794. df_drugnai["site"] = metadata[matched_idx_dn[i][:, 1] - 1, 4]
  795. df_drugnai["group"] = "drug_naive"
  796. dfi_ovr = pd.concat([df_drug, df_nodrug])
  797. dfi_dn = pd.concat([df_drug, df_drugnai])
  798. #Calculate singnificance for each frequency with linear mixed model:
  799. for fqi in range(dim_feat):
  800. #OVR
  801. dfi_ovr_fqi = dfi_ovr.iloc[:, -3:]
  802. dfi_ovr_fqi["psd"] = dfi_ovr.iloc[:, fqi]
  803. md = smf.mixedlm("psd ~ group + year_c", dfi_ovr_fqi, groups=dfi_ovr_fqi["site"])
  804. mdf = md.fit()
  805. p_ovr[i, fqi] = mdf.pvalues['group[T.drugY]']
  806. #Drug-naive
  807. dfi_dn_fqi = dfi_dn.iloc[:, -3:]
  808. dfi_dn_fqi["psd"] = dfi_dn.iloc[:, fqi]
  809. md = smf.mixedlm("psd ~ group + year_c", dfi_dn_fqi, groups=dfi_dn_fqi["site"])
  810. mdf = md.fit()
  811. p_dn[i, fqi] = mdf.pvalues['group[T.drug_naive]']
  812. #Mean over n_matching_repetitions:
  813. M_psd_drug = np.mean(psd_drug, axis=0)
  814. M_psd_nodrug = np.mean(psd_nodrug, axis=0)
  815. M_psd_drug_naive = np.mean(psd_drug_naive, axis=0)
  816. M_semI_drug = np.mean(semI_drug, axis=0)
  817. M_semI_nodrug = np.mean(semI_nodrug, axis=0)
  818. M_semI_drug_naive = np.mean(semI_drug_naive, axis=0)
  819. #Stack above matrices into one for mean features and one for SEM:
  820. M_psd_all = np.vstack([M_psd_drug, M_psd_nodrug, M_psd_drug_naive])
  821. M_sem_all = np.vstack([M_semI_drug, M_semI_nodrug, M_semI_drug_naive])
  822. #corrected p-vales
  823. rej, p_ovr_corr, sth, sth = multipletests(p_ovr.flatten(), method='holm')
  824. p_ovr_corr = p_ovr_corr.reshape(np.shape(p_ovr))
  825. rej, p_dn_corr, sth, sth = multipletests(p_dn.flatten(), method='holm')
  826. p_dn_corr = p_dn_corr.reshape(np.shape(p_dn))
  827. print('Returns: \n', "feature matrix of shape:", np.shape(M_psd_all), ', groups: drug, others, drug-naive), \n',
  828. "SEM matrix of shape: ", np.shape(M_sem_all), '\n',
  829. "p-values for ovr and drug-naive comparison for each repetition, shape:", np.shape(p_dn))
  830. return(M_psd_all, M_sem_all, p_ovr_corr, p_dn_corr)
  831. # %% [markdown]
  832. # ## PSD
  833. # %%
  834. freq_grid_nolinenoise = list(np.arange(4, 48.5, 0.5))
  835. freq_grid_nolinenoise.extend(np.arange(52, 60.5, 0.5))
  836. # %%
  837. len(freq_grid_nolinenoise)
  838. # %%
  839. #Find indexes in zdata for PSD features
  840. #widmo indexy
  841. PSD_idx = {}
  842. for ev in event_names:
  843. PSD_idx[ev] = np.zeros((np.shape(freq_grid_nolinenoise)[0], np.shape(channel_names)[0]))
  844. for ch in range(len(channel_names)):
  845. for fq in range(len(freq_grid_nolinenoise)):
  846. PSD_idx[ev][fq, ch] = int(feature_metadata.loc[((feature_metadata['measures']=='PSD') & (feature_metadata['events']==ev) & (feature_metadata['channel1']==channel_names[ch]) & (feature_metadata['freq']==freq_grid_nolinenoise[fq]))].index[0])
  847. # %%
  848. #Set which electrodes / events / medicines to plot. Here, we will use an example from Figure 4 in our manuscript.
  849. savepath_psd = os.path.join(figure_directory, 'single_features_comparisons')
  850. if not os.path.isdir(savepath_psd):
  851. os.mkdir(savepath_psd)
  852. chpsd = ['F3', 'F4', 'F7', 'F8'] #best in PCA 10 in psd
  853. ev = 'OZ'
  854. medname = "BDZ"
  855. colors = ['forestgreen', 'royalblue', 'orange']
  856. #Convert names of required channels to indices in data
  857. chpsd_idx = [list(channel_names).index(ch) for ch in chpsd]
  858. feat_idx = PSD_idx[ev][:, chpsd_idx].astype(int)
  859. #find matched indices for given drug
  860. matched_idx_ovr = matched_indices_ovr[medication_names.index(medname)]
  861. matched_idx_dn = matched_indices_drug_naive[medication_names.index(medname)]
  862. #Create matrices of mean log PSD values and their SEM for each group, and p-values for mean psd (mean over channels)
  863. #Note: it can take a while as new linear mixed model is calculated for each feature.
  864. M_psd_all, M_sem_all, p_ovr_corr, p_dn_corr = get_meanfeaturevals_and_sem(feat_idx, data, metadata, matched_idx_ovr, matched_idx_dn, transform_log=1, average_channels=1)
  865. # %%
  866. #PLOT
  867. fig, (ax1, ax15, ax2) = plt.subplots(3, 1, sharex=True, height_ratios=[9,0.5,0.5], figsize=(10*0.85,6*0.85))# hspace=0.2)
  868. for group in range(np.shape(M_psd_all)[0]):
  869. ax1.plot(freq_grid_nolinenoise, M_psd_all[group], color=colors[group])
  870. ax1.legend([medname, 'other', 'drug-naive'], fontsize="16")
  871. for group in range(np.shape(M_psd_all)[0]):
  872. ax1.fill_between(freq_grid_nolinenoise, M_psd_all[group]+2*M_sem_all[group], M_psd_all[group]-2*M_sem_all[group], alpha=.4, color=colors[group])
  873. #remove upper and right box edges
  874. ax1.spines['top'].set_visible(False)
  875. ax1.spines['right'].set_visible(False)
  876. #set y limits so the graph is readable
  877. xmax = 40
  878. plt.xlim((4, xmax))
  879. ax1.set_ylim(min(M_psd_all[:, 2*xmax-2]), max(ax1.get_ylim()))
  880. ax1.set_ylabel('Z-scored log(power)', fontsize=20) #unit of power: [V$^2$]
  881. ax1.tick_params(axis='both', labelsize=18)
  882. tit = "logPSD " + str(chpsd) + " during " + ev
  883. ax15.fill_between(x=freq_grid_nolinenoise, y1=0, y2=1, where = np.sum(p_dn_corr<alpha, axis=0) >= min_significant_matches, color=colors[2], alpha=0.5)
  884. ax15.text(-0.02, 0.5, "p<0.05 vs drug-naive", horizontalalignment='right', verticalalignment='center', size=14, transform=ax15.transAxes)
  885. ax2.fill_between(x=freq_grid_nolinenoise, y1=0, y2=1, where = np.sum(p_ovr_corr<alpha, axis=0) >= min_significant_matches, color=colors[1], alpha=0.5)
  886. ax2.text(-0.02, 0.5, "p<0.05 vs others", horizontalalignment='right', verticalalignment='center', size=14, transform=ax2.transAxes)
  887. ax2.set_xlabel("Frequency [Hz]", fontsize=20)
  888. ax2.tick_params(axis='x', labelsize=18)
  889. ax2.set_yticklabels([])
  890. ax15.set_yticklabels([])
  891. plt.tight_layout()
  892. plt.subplots_adjust(hspace=0.02)
  893. plt.savefig(os.path.join(savepath_psd, medname + '_' + tit + '.png'))
  894. plt.savefig(os.path.join(savepath_psd, medname + '_' + tit + '.svg'))
  895. plt.show()
  896. plt.close()
  897. # %% [markdown]
  898. # ## Nonlinear
  899. # %%
  900. nonlin_idx = {}
  901. for ev in event_names:
  902. nonlin_idx[ev] = {}
  903. for m in np.unique(feature_metadata.loc[feature_metadata["feat_group"]=='nonlinear', "measures"]):
  904. aa = np.zeros(np.shape(channel_names))
  905. for ch in range(0, len(channel_names)):
  906. aa[ch] = feature_metadata.loc[((feature_metadata['measures']==m) & (feature_metadata['events']==ev) & (feature_metadata['channel1']==channel_names[ch]))].index.values[0]
  907. nonlin_idx[ev][m] = aa
  908. # %%
  909. nonlin_idx[ev].keys()
  910. # %%
  911. chpsd = ['Fz', 'F3', 'Fp1', 'F4', 'Fp2']
  912. ev = 'OZ'
  913. medname = "BDZ"
  914. measure= "HFD_"
  915. #colors = ['forestgreen', 'royalblue', 'orange']
  916. #Convert names of required channels to indices in data
  917. chpsd_idx = [list(channel_names).index(ch) for ch in chpsd]
  918. feat_idx = nonlin_idx[ev][measure][chpsd_idx].astype(int)
  919. #find matched indices for given drug
  920. matched_idx_ovr = matched_indices_ovr[medication_names.index(medname)]
  921. matched_idx_dn = matched_indices_drug_naive[medication_names.index(medname)]
  922. #Create matrices of mean log PSD values and their SEM for each group, and p-values for mean psd (mean over channels)
  923. #Note: it can take a while as new linear mixed model is calculated for each feature.
  924. M_hfd_all, M_sem_all, p_ovr_corr, p_dn_corr = get_meanfeaturevals_and_sem(feat_idx, zdata, metadata, matched_idx_ovr, matched_idx_dn, transform_log=0, average_channels=1)
  925. # %%
  926. #plot
  927. medname_short = medication_short_names[medication_names.index(medname)]
  928. fig = plt.figure(figsize=(6,6))
  929. plt.bar([medname_short, "Others", "Drug \n naive"], M_hfd_all.flatten(), width=0.4)
  930. plt.errorbar([0,1,2], M_hfd_all.flatten(), yerr=M_sem_all.flatten(), fmt="o")
  931. plt.ylabel(measure.replace('_', ' ') + "\n (z-scored)", fontsize=25)
  932. plt.xticks(fontsize=24)
  933. plt.yticks(fontsize=24)
  934. plt.tight_layout()
  935. tit_png = medname + '_' + measure + '_' + str(chpsd) + ev + '.png'
  936. tit_svg = medname + '_' + measure + '_' + str(chpsd) + ev +'.svg'
  937. plt.savefig(os.path.join(savepath_psd, tit_svg))
  938. plt.savefig(os.path.join(savepath_psd, tit_png))
  939. plt.show()
  940. plt.close()
  941. # %%
  942. np.unique(feature_metadata['measures'])
  943. # %%
  944. #Indexes for connectivity measures with 0.5 Hz resolution
  945. freq_all = list(np.unique(feature_metadata.loc[feature_metadata['feat_group']=='coh_fq', 'freq']))
  946. measures = np.unique(feature_metadata.loc[feature_metadata['feat_group']=='coh_fq', 'measures'])
  947. COH_idx = {}
  948. #channel pair indexes
  949. channel_pairs = {}
  950. ichp = 0
  951. for ch1 in range(0, len(channel_names)):
  952. for ch2 in range(ch1+1, len(channel_names)):
  953. channel_pairs[ichp] = (channel_names[ch1], channel_names[ch2])
  954. ichp+=1
  955. for ev in event_names:
  956. COH_idx[ev] = {}
  957. for m in measures:
  958. COH_idx[ev][m] = np.zeros((len(channel_pairs), len(freq_all)))
  959. for fqz in freq_all:
  960. for ichp in channel_pairs:
  961. COH_idx[ev][m][ichp, freq_all.index(fqz)] = feature_metadata.loc[((feature_metadata['measures']==m) & (feature_metadata['events']==ev) & (feature_metadata['channel1']==channel_pairs[ichp][0]) & (feature_metadata['channel2']==channel_pairs[ichp][1]) & (feature_metadata['freq']== fqz))].index[0]
  962. print(m, 'index shape:', np.shape(COH_idx[ev][m]))
  963. # %%
  964. #Indexes for binned frequency measures
  965. freq_all_bin = list(np.unique(feature_metadata.loc[feature_metadata['feat_group']=='coh_bins', 'freq']))
  966. measures = np.unique(feature_metadata.loc[feature_metadata['feat_group']=='coh_bins', 'measures'])
  967. COH_bin_idx = {}
  968. for ev in event_names:
  969. COH_bin_idx[ev] = {}
  970. for m in measures:
  971. COH_bin_idx[ev][m] = np.zeros((len(channel_pairs), len(freq_all_bin)))
  972. for fqz in freq_all_bin:
  973. for ichp in channel_pairs:
  974. COH_bin_idx[ev][m][ichp, freq_all_bin.index(fqz)] = feature_metadata.loc[((feature_metadata['measures']==m) & (feature_metadata['events']==ev) & (feature_metadata['channel1']==channel_pairs[ichp][0]) & (feature_metadata['channel2']==channel_pairs[ichp][1]) & (feature_metadata['freq']== fqz))].index[0]
  975. print(m, 'index shape:', np.shape(COH_bin_idx[ev][m]))
  976. # %%
  977. ev = 'OZ'
  978. medname = "BDZ"
  979. measure= "coh"
  980. freq=813. #alpha
  981. #to see the example of the same plot but for 0.5-Hz resolution coherence measures, uncomment the following two lines:
  982. #measure= "Coherence_scipy"
  983. #freq=np.arange(8, 13.5, 0.5) #alpha range
  984. #Convert names of required channels to indices in data
  985. if measure in np.unique(feature_metadata.loc[feature_metadata['feat_group']=='coh_bins', 'measures']):
  986. feat_idx = COH_bin_idx[ev][measure][:, freq_all_bin.index(freq)].astype(int)
  987. av_chan = 0
  988. elif measure in np.unique(feature_metadata.loc[feature_metadata['feat_group']=='coh_fq', 'measures']):
  989. feat_idx = COH_idx[ev][measure][:, freq_all.index(freq[0]):freq_all.index(freq[-1])].astype(int)
  990. av_chan = 1
  991. else:
  992. print("ERROR: Measure not in allowed connectivity measures")
  993. #find matched indices for given drug
  994. matched_idx_ovr = matched_indices_ovr[medication_names.index(medname)]
  995. matched_idx_dn = matched_indices_drug_naive[medication_names.index(medname)]
  996. #Create matrices of mean log PSD values and their SEM for each group, and p-values for mean psd (mean over channels)
  997. #Note: it can take a while as new linear mixed model is calculated for each feature.
  998. M_coh_all, M_sem_all, p_ovr_corr, p_dn_corr = get_meanfeaturevals_and_sem(feat_idx, zdata, metadata, matched_idx_ovr, matched_idx_dn, transform_log=0, average_channels=av_chan)
  999. # %%
  1000. #Reshape the mean coherence matrices and p_values to n_channels x n_channels shape:
  1001. def reshape_matrices_chanelpairs(matrix_to_reshape, channel_names, channel_pairs):
  1002. channel_pairs_r = {v: k for k, v in channel_pairs.items()}
  1003. reshaped_matrix = np.zeros((np.shape(matrix_to_reshape)[0], len(channel_names), len(channel_names)))
  1004. for g in range(np.shape(matrix_to_reshape)[0]):
  1005. for ch1 in range(0, len(channel_names)):
  1006. for ch2 in range(ch1+1, len(channel_names)):
  1007. reshaped_matrix[g, ch1, ch2] = matrix_to_reshape[g, channel_pairs_r[(channel_names[ch1], channel_names[ch2])]]
  1008. reshaped_matrix[g, ch2, ch1] = matrix_to_reshape[g, channel_pairs_r[(channel_names[ch1], channel_names[ch2])]]
  1009. return reshaped_matrix
  1010. # %%
  1011. M_coh_all = reshape_matrices_chanelpairs(M_coh_all, channel_names, channel_pairs)
  1012. p_ovr_corr = reshape_matrices_chanelpairs(p_ovr_corr, channel_names, channel_pairs)
  1013. p_dn_corr = reshape_matrices_chanelpairs(p_dn_corr, channel_names, channel_pairs)
  1014. # %%
  1015. np.shape(M_coh_all), np.shape(p_ovr_corr)
  1016. # %%
  1017. #Plot three groups connectivity side by side
  1018. groups = [medname, "other", "drug-naive"]
  1019. fig, ax = plt.subplots(1,3, figsize=(18, 5))
  1020. for g in range(np.shape(M_coh_all)[0]):
  1021. plot_conn = plot_sensor_connectivity_topomap(example_raw_eeg.info, M_coh_all[g], cmap="Reds", cbar_label=None, ax=ax[g]) #, top_n=30, v_min=0.015, v_max=0.050, cmap='viridis',ax=None, save_path=None):
  1022. ax[g].text(0.5, 1.02, groups[g], horizontalalignment='center', verticalalignment='bottom', size=16, transform=ax[g].transAxes)
  1023. # %%
  1024. #Plot differences between the medicine group and comparison group - OVR
  1025. comparison2 = "other"
  1026. g2 = groups.index(comparison2)
  1027. M_coh_diff = M_coh_all[0] - M_coh_all[g2]
  1028. #remove insignificant connections
  1029. not_signif = np.sum(p_ovr_corr<alpha, axis=0) < min_significant_matches
  1030. M_coh_diff[not_signif] = 0
  1031. # %%
  1032. v_max = np.max(np.abs(M_coh_diff))
  1033. if measure in np.unique(feature_metadata.loc[feature_metadata['feat_group']=='coh_fq', 'measures']):
  1034. freq2 = str(freq[0]) + '-' + str(freq[-1]) + 'Hz'
  1035. else:
  1036. freq2 = str(freq) + 'Hz'
  1037. tit = measure + '_' + medname + '_vs_' + comparison2 + '_at_' + freq2 + '_during_' + ev
  1038. plot_sensor_connectivity_topomap(example_raw_eeg.info,
  1039. M_coh_diff,
  1040. cmap="RdBu_r", cbar_label=None,
  1041. v_min=-v_max, v_max=v_max,
  1042. top_n = min(30, np.sum(~not_signif)),
  1043. #save_path=os.path.join(savepath_psd, tit + '.png'))
  1044. save_path=None) #change to a path if you want the graph saved instead of showing

Brainwaves_Under_Medication_Visualisation_and_PCA_analysis_code_clean.ipynb at commit 99898aa, no license · at the source

Overview

Authors: Magdalena Szponar1, Patrycja Dzianok1,2, Bartłomiej Gmaj3, Wojciech Jernajczyk4, Jan Kamiński5,1
ORCID iDs: Bartłomiej Gmaj
  1. Laboratory of Neurophysiology of Mind, Nencki Institute of Experimental Biology, Warsaw, Poland
  2. International Institute of Molecular and Cell Biology in Warsaw, Warsaw, Poland
  3. Department of Psychiatry, Medical University of Warsaw, Warsaw, Poland
  4. Department of Clinical Neurophysiology, Institute of Psychiatry and Neurology, Warsaw, Poland
  5. Department of Neurosurgery, SUNY Upstate Medical University, Syracuse, NY, USA
Journal: EBioMedicine, volume 130, article 106375
Dates: received 8 December 2025; accepted 23 June 2026; published online 9 July 2026; in print August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.ebiom.2026.106375 · PMID 42424703 · PMCID PMC13380497 · OpenAlex W7167808942
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), human (organism), clinical / translational (subfield)
Methods: Connectivity, Spectral & time-frequency, Statistics, Smoothing, state filtering, decompositions, Preprocessing, Complexity, Physiology & signal measures
Keywords: Psychotropic drugs, Psychiatry, Pharmacotherapy, Mental disorders, Electroencephalography (EEG), PharmacoEEG
MeSH: Brain*, Brain Waves*, Electroencephalography*, Mental Disorders*, Psychotropic Drugs*, Cross-Sectional Studies, Female, Humans, Male (* major topic)
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: European Funds for Smart Economy; This publication was financially supported by the Foundation for Polish Science (FNP) within the BRAINCITY IRAP project, funded by the EU under the European Funds for Smart Economy programme
Citations: cited by 1 paper (Europe PMC); 99 references in the paper

Abstract

Background: Psychotropic medications remain foundational in psychiatric care, yet the neurophysiological mechanisms through which they exert therapeutic and adverse effects are still poorly characterised, limiting the field's ability to optimise treatment selection and monitoring. Electroencephalography (EEG) offers a non-invasive, real-time window into brain function that could support more precise, mechanism-informed prescribing; however, progress has been constrained by the absence of sufficiently large and systematically analysed pharmaco-EEG datasets.

Methods: In this cross-sectional observational study, we analysed over 24,000 clinical EEG recordings (∼6000 h of data) obtained across a wide range of psychiatric diagnoses and medication regimens. We compared more than 75,000 spectral, connectivity, and nonlinear EEG features across major drug classes, including benzodiazepines, SSRIs, antipsychotics, and anticonvulsants.

Findings: Dimensionality-reduced analyses revealed robust, class-specific neurophysiological signatures that can be linked to psychotropic drugs' mechanisms of action: benzodiazepines increased beta and decreased theta–alpha power; SSRIs enhanced gamma-band coherence; and antipsychotics and anticonvulsants produced marked slow-wave amplification and reductions in signal complexity. All results are made publicly accessible through an interactive resource (BrainwavesRX), enabling clinicians and researchers to explore medication-specific EEG effects at multiple levels of granularity.

Interpretation: By establishing a population-level reference atlas of psychotropic medication effects on human neural dynamics, this study provides an important foundation for future studies leveraging EEG to predict treatment response, detect insufficient or excessive pharmacological effects, and ultimately advance the development of individualised, data-driven psychiatric care.

Funding: The publication was prepared as part of Foundation of Polish Science's Proof of Concept (FENG.02.01-IP.05-0010/24) and BRAINCITY IRAP (FENG.02.07-IP.05-0179/23) projects.

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

labianca/EEG-psychotropic-medications

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 99898aadd758379da6d9f23b98763479c731c631, 7 July 2026
Languages: MATLAB (5), Jupyter (1)
Size: 7 files, 6 scripts
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: EEGLAB (1 file), h5py (1 file), ICLabel (1 file), Statistics and Machine Learning Toolbox (1 file), Matplotlib (1 file), MNE-Python (1 file), NumPy (1 file), pandas (1 file), SciPy (1 file), seaborn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
7 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;
  • 6 scripts, each with its path and the digest of its content;
  • 8 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

The results from all comparisons between all drug classes are available on the interactive website, at https://brainwavesrx.nencki.edu.pl/.

The code used for data preprocessing and statistical analysis is available on GitHub at https://github.com/labianca/EEG-psychotropic-medications.

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

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 5 authors, 6 keywords, 9 MeSH terms, 2 funders, 91 references.

Cite

This paper

Szponar, M., Dzianok, P., Gmaj, B., Jernajczyk, W., & Kamiński, J. (2026). Brainwaves under medication: revealing class-specific neural signatures of psychotropic medication from 24,000 EEGs. EBioMedicine, 130, 106375. https://doi.org/10.1016/j.ebiom.2026.106375

BibTeX

@article{szponar2026brainwaves,
author = {Szponar, Magdalena and Dzianok, Patrycja and Gmaj, Bartłomiej and Jernajczyk, Wojciech and Kamiński, Jan},
title = {{Brainwaves under medication: revealing class-specific neural signatures of psychotropic medication from 24,000 EEGs}},
journal = {EBioMedicine},
year = {2026},
month = jul,
volume = {130},
pages = {106375},
publisher = {Elsevier},
issn = {2352-3964},
doi = {10.1016/j.ebiom.2026.106375},
url = {https://doi.org/10.1016/j.ebiom.2026.106375},
pmid = {42424703},
pmcid = {PMC13380497}
}

RIS

TY - JOUR
AU - Szponar, Magdalena
AU - Dzianok, Patrycja
AU - Gmaj, Bartłomiej
AU - Jernajczyk, Wojciech
AU - Kamiński, Jan
TI - Brainwaves under medication: revealing class-specific neural signatures of psychotropic medication from 24,000 EEGs
T2 - EBioMedicine
J2 - eBioMedicine
PY - 2026
DA - 2026/07/09
VL - 130
SP - 106375
SN - 2352-3964
PB - Elsevier
DO - 10.1016/j.ebiom.2026.106375
UR - https://doi.org/10.1016/j.ebiom.2026.106375
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.ebiom.2026.106375",
"type": "article-journal",
"title": "Brainwaves under medication: revealing class-specific neural signatures of psychotropic medication from 24,000 EEGs",
"container-title": "EBioMedicine",
"author": [
{
"family": "Szponar",
"given": "Magdalena"
},
{
"family": "Dzianok",
"given": "Patrycja"
},
{
"family": "Gmaj",
"given": "Bartłomiej"
},
{
"family": "Jernajczyk",
"given": "Wojciech"
},
{
"family": "Kamiński",
"given": "Jan"
}
],
"container-title-short": "eBioMedicine",
"volume": "130",
"page": "106375",
"DOI": "10.1016/j.ebiom.2026.106375",
"PMID": "42424703",
"PMCID": "PMC13380497",
"ISSN": "2352-3964",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.ebiom.2026.106375",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
9
]
]
}
}

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/fpsyt.2026.1737357 [code]
A computational pipeline for a neurotransmitter-centric analysis of the effects of psychiatric medication on EEG spectral power.
Journal: Frontiers in psychiatry
In common: EEGLAB, Statistics and Machine Learning Toolbox, EEG, 6 references
[2] doi:10.1007/s10548-026-01238-y [code]
Topographic Reorganization of EEG Complexity During Visual Mental Imagery: Insights from Lempel-Ziv Complexity in High-Density EEG.
Journal: Brain topography
In common: ICLabel, MNE-Python, statsmodels, 4 other tools, EEG, 2 references
[3] doi:10.1002/mds.70348 [code]
Electroencephalography-Based Clustering Reveals Robust Neurophysiological Subtypes in Parkinson's Disease.
Journal: Movement disorders : official journal of the Movement Disorder Society
In common: ICLabel, EEGLAB, MNE-Python, 6 other tools, EEG
[4] doi:10.7554/elife.107088 [code]
Development of auditory and spontaneous movement responses to music over the first postnatal year.
Journal: eLife
In common: ICLabel, EEGLAB, MNE-Python, 6 other tools, EEG
[5] doi:10.1162/imag.a.1229 [code]
40 Hz audiovisual stimulation improves sustained attention and related brain oscillations.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: ICLabel, EEGLAB, Statistics and Machine Learning Toolbox, 5 other tools, EEG, 1 reference
[6] doi:10.2196/80286 [code]
At-Home Sleep Electroencephalography Assessment in Young and Older Adults Using a Novel Wireless Soft Electronics Sleep Monitoring System: Experimental Study.
Journal: JMIR formative research
In common: EEGLAB, MNE-Python, Statistics and Machine Learning Toolbox, 5 other tools, EEG, 1 reference
[7] doi:10.1162/imag.a.1245 [code]
Towards precision EEG connectomics: Evaluating the benefits of dense sampling.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: ICLabel, MNE-Python, statsmodels, 6 other tools, EEG
[8] doi:10.1162/imag.a.105 [code]
Right posterior theta reflects human parahippocampal phase resetting by salient cues during goal-directed navigation
Journal: n/a
In common: EEGLAB, MNE-Python, statsmodels, 6 other tools, EEG
[9] doi:10.1038/s41598-026-56070-y [code]
SSDLabeler: realistic semi-synthetic data generation for multi-label artifact classification in EEG.
Journal: Scientific reports
In common: ICLabel, EEGLAB, statsmodels, 5 other tools, EEG
[10] doi:10.3389/fncom.2026.1786996 [code]
Schumann-anchored golden ratio organization of human neural oscillations.
Journal: Frontiers in computational neuroscience
In common: MNE-Python, h5py, statsmodels, 5 other tools, EEG, 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.