OSCR

Dynamic neural states underpin motor symptom severity in Parkinson's disease: a longitudinal analysis of chronic cortico-subthalamic nucleus recordings.

Code ↔ Paper

9 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 9 matches
  1. [1] § Methods › Modelling symptom severity using static spectral features ↔ eBioMed_postAnalysis_bradykinesia.py, lines 525–542 · score 0.77 · 20–35 Hz, 40–70 Hz, 70–100 Hz, delta alpha, low gamma, low beta
  2. [2] § Methods › Modelling symptom severity using HMM derived spatio-spectral features ↔ eBioMed_postAnalysis_bradykinesia.py, lines 525–542 · score 0.77 · 20–35 Hz, 40–70 Hz, 70–100 Hz, delta alpha, low gamma, low beta
  3. [3] § Methods › Modelling symptom severity using HMM derived temporal features ↔ eBioMed_postAnalysis_HMM_dyskinesia.py, lines 266–379 · score 0.66 · avoid multicollinearity, independent variable, temporal properties, fit, dyskinesia, GLM
  4. [4] § Methods › Modelling symptom severity using HMM derived temporal features ↔ eBioMed_postAnalysis_HMM_tremor.py, lines 206–332 · score 0.66 · avoid multicollinearity, independent variable, temporal properties, fit, tremor, GLM
  5. [5] § Methods › Hidden Markov Model fit ↔ SpectralUpdateHMM.py, lines 208–248 · score 0.57 · OSL dynamics toolbox, Python, dimensional, matrix, window, channel
  6. [6] § Methods › Static spectral analysis ↔ eBioMed_DualEstimation_HMM.py, lines 604–658 · score 0.56 · 2–100 Hz, power spectral densities, window, PSDs, coherence, HMM
  7. [7] § Results › Spectral fingerprints of Cortico-Subthalamic states ↔ eBioMed_postAnalysis_spectral_and_temporal_prop_plots_quantiles.py, lines 14–64 · score 0.56 · Hidden Markov Model, power spectral density, bradykinesia score, HMM state, wirelessly, Parkinson
  8. [8] § Results › Spectral fingerprints of Cortico-Subthalamic states ↔ eBioMed_postAnalysis_spectral_and_temporal_prop_plots_quantiles.py, lines 14–64 · score 0.55 · hidden Markov model, power spectral density, Spectral properties, bradykinesia scores, standard error, shifts
  9. [9] § Methods › Modelling symptom severity using static spectral features ↔ eBioMed_postAnalysis_HMM_dyskinesia.py, lines 439–451 · score 0.51 · delta alpha, static spectral, low gamma, low beta, high beta, high gamma

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 1,827 lines · 71 KB · AGPL-3.0 · 2 matches

  1. # %% IMPORTS
  2. import os
  3. import pickle
  4. import pandas as pd
  5. import numpy as np
  6. import statsmodels.api as sm
  7. from datetime import datetime
  8. from copy import deepcopy
  9. import matplotlib.pyplot as plt
  10. import seaborn as sns
  11. from utils import list_drives_with_names, format_func_single
  12. from SpectralUpdateHMM import SpectralUpdateHMM
  13. from statsmodels.stats.multitest import multipletests
  14. # %%
  15. """
  16. Script documentation
  17. Bradykinesia related analysis for the HMM model with dual estimated time windows
  18. Author: Abhinav Sharma ([email hidden])
  19. """
  20. # %% # ^ SET THE PATHS
  21. # Autodetect the drive letter for the external hard drive with a specific name
  22. use_external_drive = False
  23. drive_name = 'salTnsr_X10'
  24. drive_letter = None
  25. drives = list_drives_with_names()
  26. for drive in drives:
  27. if drive[1] == drive_name:
  28. drive_letter = drive[0]
  29. drive_letter = drive_letter[0]
  30. break
  31. # %% # ^ CREATE A DATAFRAME
  32. # *This is going to cycle through all participants and get the data
  33. # *Walk through the folder, the folder can contain subfolders
  34. # *We will continue to walk the folder until we find the pickle files
  35. subjectID = ''
  36. # side = 'left'
  37. ipsi_contra = 'contra'
  38. all_dicts = []
  39. # Four state model
  40. folder_path = r'C:\Oxford\data\neural\uscf_results\HMM_masterModelMonday_June_10_2024_08_34_15\dual_estimation'
  41. static_spectral_folder = r'C:\Oxford\data\neural\uscf_results\HMM_masterModelMonday_June_10_2024_08_34_15\static_spectra'
  42. ground_truth_states = 4
  43. model_name = "HMM_masterModelMonday_June_10_2024_08_34_15"
  44. if use_external_drive:
  45. folder_path = f'{drive_letter}:' + folder_path[2:]
  46. for root, dirs, files in os.walk(folder_path):
  47. for file in files:
  48. if file.endswith('.pkl') and ipsi_contra in file and (subjectID if 'RCS' in subjectID else '') in file:
  49. file_path = os.path.join(root, file)
  50. # Search for the corresponding static spectral file
  51. static_spectral_file = file_path.replace('dual_estimation', 'static_spectra')
  52. static_spectral_file = static_spectral_file.replace('dualProps', 'staticSpectra')
  53. with open(static_spectral_file, 'rb') as f:
  54. static_spectral_dict = pickle.load(f)
  55. with open(file_path, 'rb') as f:
  56. # try:
  57. dicts_from_file = pickle.load(f)
  58. # We will only select specific properties to test from
  59. # the dictionaries
  60. # Get only the file name from the path
  61. file_name = os.path.basename(file_path)
  62. print('Loaded file:', file_name)
  63. # Get the static spectra file name
  64. static_spectral_file_name = os.path.basename(static_spectral_file)
  65. print('Loaded static spectral file:', static_spectral_file_name)
  66. # Print some blank lines for separation
  67. print('\n\n')
  68. # Check if the number at the end of the spectral file name
  69. # matches the number at the end of the dual estimation file name
  70. if file_name.split('_')[-1].split('.')[0] != static_spectral_file_name.split('_')[-1].split('.')[0]:
  71. print('File name mismatch:', file_name, static_spectral_file_name)
  72. raise ValueError('File name mismatch')
  73. for d, spect in zip(dicts_from_file, static_spectral_dict):
  74. new_dict = {}
  75. new_dict['med_status'] = d['medication_status']
  76. # Get the static spectral properties
  77. new_dict['static_coh'] = spect['coh']
  78. new_dict['f'] = spect['f']
  79. new_dict['static_psd'] = spect['psd']
  80. # Get the spectral properties for th HMM
  81. new_dict['HMM_coh'] = d['coh']
  82. new_dict['HMM_psd'] = d['psd']
  83. new_dict['HMM_f'] = d['f']
  84. new_dict['dyskinesia_score'] = d['dyskinesia_y']
  85. new_dict['tremor_score'] = d['Tremor_Score_y']
  86. new_dict['bradykinesia_score'] = d['bradykinesia_y']
  87. new_dict['bradykinesia_transformed'] = d['bradykinesia_transformed_y']
  88. # normed scores
  89. new_dict['norm_dyskinesia_score'] = d['norm_dyskinesia']
  90. new_dict['norm_bradykinesia_score'] = d['norm_bradykinesia']
  91. # status
  92. new_dict['tremor_status'] = d['Tremor_y']
  93. new_dict['behavior_status_dysk'] = d['behavior_status_dysk']
  94. # temporal properties
  95. new_dict['mean_lifetimes'] = d['mean_lifetimes']
  96. new_dict['mean_intervals'] = d['mean_intervals']
  97. new_dict['FO'] = d['fractional_occ']
  98. new_dict['switching_rate'] = d['switching_rates']
  99. # Ten minute medication status based on majority voting
  100. new_dict['ten_minute_med_status'] = d['ten_minute_med_status']
  101. # Sleep status
  102. new_dict['sleep_status'] = d['sleep_status']
  103. new_dict['ten_minute_sleep_status'] = d['ten_minute_sleep_status']
  104. # Transition matrix
  105. # new_dict['transition_matrix'] = d['transition_matrix']
  106. d_one, _ = format_func_single(d['transition_matrix'], modelStates=ground_truth_states)
  107. new_dict['transition_matrix'] = d_one[0]
  108. # Check the shape of the transition matrix
  109. # It should be a square matrix
  110. # With the number of rows and columns equal to the number of ground truth states
  111. if new_dict['transition_matrix'].shape[0] != ground_truth_states:
  112. print('Transition matrix shape mismatch:', new_dict['transition_matrix'].shape)
  113. raise ValueError('Transition matrix shape mismatch')
  114. # subject ID
  115. if subjectID:
  116. new_dict['subjectID'] = subjectID
  117. else:
  118. new_dict['subjectID'] = file_name.split('_')[0]
  119. new_dict['file_number'] = file_name.split('_')[-1].split('.')[0]
  120. all_dicts.append(new_dict)
  121. # except:
  122. # print('Error loading file:', file_path)
  123. # continue
  124. # all_dicts.extend(dicts_from_file)
  125. final_df = pd.DataFrame(all_dicts)
  126. if final_df.empty:
  127. print('No data found, maybe check your folder path')
  128. # %% # ^ REARRANGE THE DATAFRAME
  129. # & CREATE A NEW DATAFRAME
  130. # Let us rearrange the dataframe above to expand the temporal properties into separate columns for each state
  131. # This applies to the temporal properties only
  132. # We will create a new dataframe with the temporal properties expanded
  133. # We will have a column for each state
  134. # We will keep the medication status and the scores as they are
  135. clincial_df = []
  136. for i_loc,row in final_df.iterrows():
  137. row_dict = {}
  138. row_dict['med_status'] = row['med_status']
  139. row_dict['dyskinesia_score'] = row['dyskinesia_score']
  140. row_dict['tremor_score'] = row['tremor_score']
  141. row_dict['bradykinesia_score'] = row['bradykinesia_score']
  142. row_dict['bradykinesia_transformed'] = row['bradykinesia_transformed']
  143. row_dict['norm_dyskinesia_score'] = row['norm_dyskinesia_score']
  144. row_dict['norm_bradykinesia_score'] = row['norm_bradykinesia_score']
  145. row_dict['tremor_status'] = row['tremor_status']
  146. row_dict['behavior_status_dysk'] = row['behavior_status_dysk']
  147. # Static spectral properties
  148. row_dict['static_coh'] = row['static_coh']
  149. row_dict['static_psd'] = row['static_psd']
  150. row_dict['f'] = row['f']
  151. # HMM spectral properties
  152. row_dict['HMM_coh'] = row['HMM_coh']
  153. row_dict['HMM_psd'] = row['HMM_psd']
  154. row_dict['HMM_f'] = row['HMM_f']
  155. # Ten minute medication status
  156. row_dict['ten_minute_med_status'] = row['ten_minute_med_status']
  157. # Sleep status
  158. row_dict['sleep_status'] = row['sleep_status']
  159. row_dict['ten_minute_sleep_status'] = row['ten_minute_sleep_status']
  160. # Subject ID
  161. row_dict['subjectID'] = row['subjectID']
  162. # File number
  163. row_dict['file_number'] = row['file_number']
  164. # Avoid starting from state number 0 as names
  165. for i in range(ground_truth_states):
  166. row_dict[f'mean_lifetimes_{i+1}'] = row['mean_lifetimes'][i]
  167. row_dict[f'mean_intervals_{i+1}'] = row['mean_intervals'][i]
  168. row_dict[f'FO_{i+1}'] = row['FO'][i]
  169. row_dict[f'switching_rate_{i+1}'] = row['switching_rate'][i]
  170. # Refashion the transition matrix
  171. for i in range(ground_truth_states):
  172. for j in range(ground_truth_states):
  173. row_dict[f'transition_prob_{i+1}_to_{j+1}'] = row['transition_matrix'][i][j]
  174. # Append the row to the list
  175. clincial_df.append(row_dict)
  176. # Create the final dataframe
  177. final_clinical_df = pd.DataFrame(clincial_df)
  178. # Delete redundant variables to free up memory
  179. del clincial_df, all_dicts, final_df
  180. # %% # ^ STATIC SPECTRA PLOTS
  181. # ^ ---------------------------------STATIC SPECTRAL ANALYSIS--------------------------------
  182. # ^ -----------------------------------------------------------------------------------------
  183. # ^ -----------------------------------------------------------------------------------------
  184. # ^ -----------------------------------------------------------------------------------------
  185. # ^ -----------------------------------------------------------------------------------------
  186. filtered_df = final_clinical_df[final_clinical_df['med_status'].isin(['med_on', 'med_off', 'med_sleep'])]
  187. # filtered_df = final_clinical_df[final_clinical_df['med_status'].isin(['med_sleep'])]
  188. # Re-arrange the static spectral properties to plot them
  189. psd = filtered_df['static_psd'].tolist()
  190. psd = np.array(psd)
  191. med_status = filtered_df['med_status'].tolist()
  192. med_status = np.array(med_status)
  193. f = filtered_df['f'].tolist()
  194. f = np.array(f)[0, :]
  195. # Find indices corresponding to following frequencies: 2.0 and 45.0
  196. lower_index = np.argmin(np.abs(f - 2.0))
  197. upper_index = np.argmin(np.abs(f - 45.0))
  198. # Slice psd according to the indices
  199. psd = psd[:, :, lower_index:upper_index]
  200. # Averaging the STNs together and the motor cortices together
  201. psd1 = psd[:, 0:1, :].mean(axis=1)
  202. psd2 = psd[:, 2:3, :].mean(axis=1)
  203. # Concatenate the averaged PSDs
  204. # insert a new dimension to the psd1 and psd2 arrays
  205. psd1 = np.expand_dims(psd1, axis=1)
  206. psd2 = np.expand_dims(psd2, axis=1)
  207. # Concatenate the arrays
  208. psd = np.concatenate((psd1, psd2), axis=1)
  209. # Group PSDs based on medication status
  210. med_on_psd = psd[med_status == 'med_on']
  211. med_off_psd = psd[med_status == 'med_off']
  212. # Calculate the mean and standard error across the window dimension for each group
  213. med_on_psd_mean = med_on_psd.mean(axis=0)
  214. med_off_psd_mean = med_off_psd.mean(axis=0)
  215. med_on_psd_se = med_on_psd.std(axis=0) / np.sqrt(med_on_psd.shape[0])
  216. med_off_psd_se = med_off_psd.std(axis=0) / np.sqrt(med_off_psd.shape[0])
  217. # Sleep
  218. med_sleep_psd = psd[med_status == 'med_sleep']
  219. med_sleep_psd_mean = med_sleep_psd.mean(axis=0)
  220. med_sleep_psd_se = med_sleep_psd.std(axis=0) / np.sqrt(med_sleep_psd.shape[0])
  221. # Set font to Arial
  222. plt.rcParams['font.family'] = 'Arial'
  223. # Define colors and channel names
  224. # colors = {'med_on': '0c5ac', 'med_off': 'cf560a'}
  225. # channel_names = ['STN1', 'STN2', 'Motor Cortex 1', 'Motor Cortex 2']
  226. channel_names = ['STN', 'Cortex']
  227. # Create subplots
  228. fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8, 4))
  229. axes = axes.flatten()
  230. for channel in range(psd.shape[1]):
  231. ax = axes[channel]
  232. # Plot med_on PSD
  233. ax.plot(f[lower_index:upper_index], med_on_psd_mean[channel, :], label='low bradykinesia', color='#cf560a', linewidth=2)
  234. ax.fill_between(f[lower_index:upper_index],
  235. med_on_psd_mean[channel, :] - med_on_psd_se[channel, :],
  236. med_on_psd_mean[channel, :] + med_on_psd_se[channel, :],
  237. color='#cf560a', alpha=0.3)
  238. # Plot med_off PSD
  239. ax.plot(f[lower_index:upper_index], med_off_psd_mean[channel, :], label='high bradykinesia', color='#0a69cf', linewidth=2, linestyle='--')
  240. ax.fill_between(f[lower_index:upper_index],
  241. med_off_psd_mean[channel, :] - med_off_psd_se[channel, :],
  242. med_off_psd_mean[channel, :] + med_off_psd_se[channel, :],
  243. color='#0a69cf', alpha=0.3)
  244. # Plot med_sleep PSD
  245. ax.plot(f[lower_index:upper_index], med_sleep_psd_mean[channel, :], label='sleep', color='#25be72', linewidth=2)
  246. ax.fill_between(f[lower_index:upper_index],
  247. med_sleep_psd_mean[channel, :] - med_sleep_psd_se[channel, :],
  248. med_sleep_psd_mean[channel, :] + med_sleep_psd_se[channel, :],
  249. color='#25be72', alpha=0.3)
  250. # Set labels and title
  251. ax.set_title(channel_names[channel], fontsize=16)
  252. ax.set_xlabel('Frequency (Hz)', fontsize=16)
  253. ax.set_ylabel('Power Spectral Density (PSD)', fontsize=16)
  254. ax.set_yscale('log')
  255. ax.legend(fontsize=10)
  256. ax.grid(True, linestyle='--', alpha=0.6)
  257. # Set xtick font and fontsize
  258. for tick in ax.xaxis.get_major_ticks():
  259. tick.label1.set_fontsize(16)
  260. # Set ytick font and fontsize
  261. for tick in ax.yaxis.get_major_ticks():
  262. tick.label1.set_fontsize(16)
  263. # Adjust layout
  264. plt.tight_layout()
  265. plt.show()
  266. fig.savefig(r'C:\Oxford\software\Movement_disorders_revision\static_spectral_properties_avrg_with_sleep.png', dpi=600)
  267. fig.savefig(r'C:\Oxford\software\Movement_disorders_revision\static_spectral_properties_avrg_with_sleep.svg', dpi=600)
  268. # % Percentile based psds
  269. filtered_df = final_clinical_df[(final_clinical_df['bradykinesia_transformed'] <= 80)]
  270. # Define the percentiles
  271. percentiles = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
  272. f = filtered_df['f'].tolist()
  273. f = np.array(f)[0, :]
  274. # Find indices corresponding to following frequencies: 2.0 and 45.0
  275. lower_index = np.argmin(np.abs(f - 2.0))
  276. upper_index = np.argmin(np.abs(f - 45.0))
  277. fig, ax = plt.subplots(figsize=(6, 6))
  278. for i, percentile in enumerate(percentiles):
  279. bradykinesia_percentile = np.percentile(filtered_df['bradykinesia_transformed'], percentile)
  280. # Filter the dataframe based on the percentile
  281. percentile_df = filtered_df[(filtered_df['bradykinesia_transformed'] <= bradykinesia_percentile)]
  282. # Static spectra
  283. psd = percentile_df['static_psd'].tolist()
  284. psd = np.array(psd)
  285. # Slice psd according to the indices
  286. psd = psd[:, :, lower_index:upper_index]
  287. # Averaging the STNs together and the motor cortices together
  288. psd_stn = psd[:, 0:1, :].mean(axis=1)
  289. psd_stn_mean = psd_stn.mean(axis=0)
  290. # psd_ctx = psd[:, 2:3, :].mean(axis=1)
  291. # psd_ctx_mean = psd_ctx.mean(axis=0)
  292. # Plot the STN PSD
  293. sns.lineplot(x=f[lower_index:upper_index], y=psd_stn_mean,
  294. label = f'{percentile}th percentile',
  295. ax=ax, linewidth=1)
  296. # Set labels and title
  297. ax.set_title('STN LOG', fontsize=16)
  298. ax.set_xlabel('Frequency (Hz)', fontsize=16)
  299. ax.set_ylabel('Power Spectral Density (PSD)', fontsize=16)
  300. ax.set_yscale('log')
  301. # ax.set_yscale('linear')
  302. ax.grid(True, linestyle='--', alpha=0.6)
  303. # Set xtick font and fontsize
  304. for tick in ax.xaxis.get_major_ticks():
  305. tick.label1.set_fontsize(16)
  306. # Set ytick font and fontsize
  307. for tick in ax.yaxis.get_major_ticks():
  308. tick.label1.set_fontsize(16)
  309. # Add percentile legend
  310. percentile_labels = [f'{percentile}th percentile' for percentile in percentiles]
  311. # Add the legend
  312. # ax.legend(percentile_labels, fontsize=10)
  313. # Display the legend outside the plot
  314. # plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=10)
  315. # Adjust layout
  316. plt.tight_layout()
  317. plt.show()
  318. # Save a very high resolution image
  319. fig.savefig(r'C:\Oxford\software\Movement_disorders_revision\STN_LOG_static_spectral_properties_avrg_percentile.png', dpi=600)
  320. # & Plot the static spectral properties
  321. filtered_df = final_clinical_df[final_clinical_df['med_status'].isin(['med_on', 'med_off'])]
  322. # We will have to re-arrange the static spectral properties to plot them
  323. static_df = []
  324. tsdf = []
  325. window_number = 0
  326. psd = filtered_df['static_psd'].tolist()
  327. med_status = filtered_df['med_status'].tolist()
  328. f = filtered_df['f'].tolist()
  329. for i, p in enumerate(psd):
  330. for j, p1 in enumerate(p):
  331. for k, f1 in enumerate(f[j]):
  332. temp_static_df = {}
  333. temp_static_df['f'] = f1
  334. temp_static_df['psd'] = p1
  335. temp_static_df['med_status'] = med_status[i]
  336. temp_static_df['window_number'] = i * len(p) + j + 1
  337. temp_static_df['channel_number'] = k
  338. tsdf.append(temp_static_df)
  339. static_df = pd.DataFrame(tsdf)
  340. # % Plotting the static spectral properties
  341. fig, ax = plt.subplots(figsize=(50, 8))
  342. sns.lineplot(data=static_df, x='f', y='psd', hue='med_status', ax=ax, errorbar=('ci', 95))
  343. # All the x ticks should be shown
  344. # There are 196 x ticks with unique values of f in the static_df being the labels
  345. # We will show all the x ticks
  346. ax.set_xticks(static_df['f'].unique())
  347. ax.set_xticklabels(static_df['f'].unique(), rotation=90)
  348. plt.show()
  349. # We will create four subplots for the static spectral properties for each channel
  350. fig, axs = plt.subplots(4, 1, figsize=(60, 40))
  351. for i, ax in enumerate(axs.flatten()):
  352. sns.lineplot(data=static_df[static_df['channel_number'] == i], x='f', y='psd', hue='med_status', ax=ax, errorbar=('ci', 95))
  353. ax.set_title(f'Channel {i}')
  354. ax.set_xticks(static_df['f'].unique())
  355. ax.set_xticklabels(static_df['f'].unique(), rotation=90)
  356. # ^ ---------------------------------END OF STATIC SPECTRAL ANALYSIS-------------------------
  357. # ^ -----------------------------------------------------------------------------------------
  358. # ^ -----------------------------------------------------------------------------------------
  359. # ^ -----------------------------------------------------------------------------------------
  360. # ^ -----------------------------------------------------------------------------------------
  361. # %% # ^ LOAD THE PREVIOUSLY EXISTING DATA TO UPDATE THE SPECTRAL PROPERTIES
  362. # & LOAD THE PREVIOUSLY SAVED DATA
  363. # Load the final_clinical_df
  364. model_name = 'HMM_masterModelMonday_June_10_2024_08_34_15'
  365. final_clinical_df = pd.read_pickle(f'{model_name}_clinical_scores.pkl')
  366. # %% # ^ FILTER THE DATA TO SAVE MEMORY
  367. # & Filter data based on the medication status
  368. # # Filter the final_clinical_df
  369. # filtered_df = final_clinical_df[final_clinical_df['med_status'].isin(['med_on', 'med_off'])]
  370. # del final_clinical_df
  371. # & Filter data based on the sleep status (keep both awake and sleep data)
  372. filtered_df = final_clinical_df[final_clinical_df['sleep_status'].isin(['awake', 'asleep'])]
  373. del final_clinical_df
  374. # %% # ^ CHANNEL GROUPING
  375. """ Channels and spectral grouping for averaging and creating spectral band entries in the dataframe
  376. """
  377. # These are bipolar channels
  378. node_names = ['LFP1', 'LFP2', 'ECoG1', 'ECoG2']
  379. node_identifiers = [0, 1, 2, 3]
  380. # lfp1-ecog1, lfp1-ecog2, lfp2-ecog1, lfp2-ecog2 are all the same
  381. # The resulting tuples for the above combinations are: (0, 2), (0, 3), (1, 2), (1, 3)
  382. # These will all be averaged to get the coherence between the two channels: lfp-ecog
  383. lfp_ecog = [(0, 2), (0, 3), (1, 2), (1, 3)]
  384. # The two other unique combinations are: lfp1-lfp2, ecog1-ecog2
  385. # The resulting tuples for the above combinations are: (0, 1), (2, 3)
  386. lfp1_lfp2 = [(0, 1)]
  387. ecog1_ecog2 = [(2, 3)]
  388. # %% # ^ SPECTRAL BAND DEFINITIONS
  389. delta_alpha_group = (2, 9)
  390. low_beta_group = (10, 20) # 10 20
  391. high_beta_group = (20, 35) # 20 35
  392. low_gamma_group = (40, 70) # this is fine
  393. high_gamma_group = (70, 100) # this is fine
  394. groups = {'delta_alpha': delta_alpha_group,
  395. 'low_beta': low_beta_group,
  396. 'high_beta': high_beta_group,
  397. 'low_gamma': low_gamma_group,
  398. 'high_gamma': high_gamma_group}
  399. coarse_beta_group = (12, 40)
  400. coarse_gamma_group = (70, 100)
  401. coarse_groups = {'coarse_beta': coarse_beta_group,
  402. 'coarse_gamma': coarse_gamma_group}
  403. # %% # ^ UPDATE THE SPECTRAL PROPERTIES
  404. # % Update the HMM coherene and reorganize the data
  405. # Path to the directory containing the NNMF projectors
  406. path_NNMF = r'C:\Oxford\software\wireless\results\HMM_masterModelMonday_June_10_2024_08_34_15\NNMF_factors'
  407. # Instantiate the class
  408. spectral_update = SpectralUpdateHMM(filtered_df,
  409. node_names,
  410. node_identifiers,
  411. lfp_ecog,
  412. lfp1_lfp2,
  413. ecog1_ecog2,
  414. groups,
  415. path_NNMF)
  416. # Check if the dataframe is correct
  417. spectral_update.check_df()
  418. spectral_update.update_coherence()
  419. spectral_update.update_coherence_unprojected()
  420. spectral_update.update_psd()
  421. spectral_update.update_psd_unprojected()
  422. del filtered_df
  423. filtered_df = deepcopy(spectral_update.df)
  424. # %%Now for every group find the upper and lower indices corresponding to the frequency vector
  425. # and the group definitions above
  426. xx = filtered_df['f'].iloc[0]
  427. groups_indices = {}
  428. for group_name, group in groups.items():
  429. lower_index = np.argmin(np.abs(xx - group[0]))
  430. upper_index = np.argmin(np.abs(xx - group[1]))
  431. groups_indices[group_name] = (lower_index, upper_index)
  432. coarse_groups_indices = {}
  433. for group_name, group in coarse_groups.items():
  434. lower_index = np.argmin(np.abs(xx - group[0]))
  435. upper_index = np.argmin(np.abs(xx - group[1]))
  436. coarse_groups_indices[group_name] = (lower_index, upper_index)
  437. # %% # ^ STATIC SPECTRAL PROPERTIES UPDATE
  438. """
  439. & PSD caclulations
  440. ^ The following groups are created
  441. * 1) Refined spectral bands with all four channels stored within the same group
  442. * 2) Coarse spectral bands with all four channels stored within the same group
  443. """
  444. # Calculating average spectral band psd values for each group
  445. for group_name, group in groups_indices.items():
  446. filtered_df[f'{group_name}_psd'] = filtered_df['static_psd'].apply(lambda x: x[:,group[0]:group[1]].mean(axis=1))
  447. for group_name, group in coarse_groups_indices.items():
  448. filtered_df[f'{group_name}_psd'] = filtered_df['static_psd'].apply(lambda x: x[:,group[0]:group[1]].mean(axis=1))
  449. """
  450. & PSD caclulations
  451. ^ The following groups are created
  452. * 1) Refined spectral bands with four channels completely separated
  453. * 2) Coarse spectral bands with four channels completely separated
  454. * 3) Refined spectral bands with lfp and ecog channels separated (lfp1 and lfp2 are averaged, ecog1 and ecog2 are averaged)
  455. * 4) Coarse spectral bands with lfp and ecog channels separated (lfp1 and lfp2 are averaged, ecog1 and ecog2 are averaged)
  456. """
  457. for nm in node_names:
  458. for group_name, group in groups_indices.items():
  459. filtered_df[f'{nm}_{group_name}_psd'] = filtered_df['static_psd'].apply(lambda x: x[node_identifiers[node_names.index(nm)], group[0]:group[1]].mean())
  460. for nm in node_names:
  461. for group_name, group in coarse_groups_indices.items():
  462. filtered_df[f'{nm}_{group_name}_psd'] = filtered_df['static_psd'].apply(lambda x: x[node_identifiers[node_names.index(nm)], group[0]:group[1]].mean())
  463. # Let us also calculate the average psd for lfps and ecogs separately
  464. for group_name, group in groups_indices.items():
  465. filtered_df[f'lfp_{group_name}_psd'] = filtered_df['static_psd'].apply(lambda x: np.mean([x[0, group[0]:group[1]], x[1, group[0]:group[1]]]))
  466. for group_name, group in coarse_groups_indices.items():
  467. filtered_df[f'lfp_{group_name}_psd'] = filtered_df['static_psd'].apply(lambda x: np.mean([x[0, group[0]:group[1]], x[1, group[0]:group[1]]]))
  468. for group_name, group in groups_indices.items():
  469. filtered_df[f'ecog_{group_name}_psd'] = filtered_df['static_psd'].apply(lambda x: np.mean([x[2, group[0]:group[1]], x[3, group[0]:group[1]]]))
  470. for group_name, group in coarse_groups_indices.items():
  471. filtered_df[f'ecog_{group_name}_psd'] = filtered_df['static_psd'].apply(lambda x: np.mean([x[2, group[0]:group[1]], x[3, group[0]:group[1]]]))
  472. # %% # ^ STATIC COHERENCE CALCULATIONS UPDATE
  473. """
  474. & Coherence calculations
  475. * Coherence is a matrix of shape (4, 4, n) where n is the number of frequency points
  476. * We will calculate the average coherence for each entry in the matrix and
  477. * average across based on the groups defined above for the frequency bands
  478. * In the end for a spectral band we will have a 4x4 matrix with the average coherence values
  479. """
  480. for group_name, group in groups_indices.items():
  481. filtered_df[f'{group_name}_coh'] = filtered_df['static_coh'].apply(lambda x: x[:, :, group[0]:group[1]].mean(axis=2))
  482. for group_name, group in coarse_groups_indices.items():
  483. filtered_df[f'{group_name}_coh'] = filtered_df['static_coh'].apply(lambda x: x[:, :, group[0]:group[1]].mean(axis=2))
  484. # & Coherence calculations
  485. # * We will also calculate the spectral bands for lfp-lfp and ecog-ecog pairs
  486. # ^ fine spectral bands
  487. # For lfp-lfp pair
  488. for group_name, group in groups_indices.items():
  489. for nm in lfp1_lfp2:
  490. filtered_df[f'lfp_lfp_{group_name}_coh'] = filtered_df['static_coh'].apply(lambda x: x[nm[0], nm[1], group[0]:group[1]].mean(axis=0))
  491. # For the eco-ecog pair
  492. for group_name, group in groups_indices.items():
  493. for nm in ecog1_ecog2:
  494. filtered_df[f'ecog_ecog_{group_name}_coh'] = filtered_df['static_coh'].apply(lambda x: x[nm[0], nm[1], group[0]:group[1]].mean(axis=0))
  495. # ^ coarse spectral bands
  496. # For lfp-lfp pair
  497. for group_name, group in coarse_groups_indices.items():
  498. for nm in lfp1_lfp2:
  499. filtered_df[f'lfp_lfp_{group_name}_coh'] = filtered_df['static_coh'].apply(lambda x: x[nm[0], nm[1], group[0]:group[1]].mean(axis=0))
  500. # For the eco-ecog pair
  501. for group_name, group in coarse_groups_indices.items():
  502. for nm in ecog1_ecog2:
  503. filtered_df[f'ecog_ecog_{group_name}_coh'] = filtered_df['static_coh'].apply(lambda x: x[nm[0], nm[1], group[0]:group[1]].mean(axis=0))
  504. # & Coherence calculations
  505. # * Okay so now we will calculate spectral bands for lfp-ecog pairs and average across the pairs
  506. # ^ fine spectral bands
  507. for group_name, group in groups_indices.items():
  508. # Add an empty column to the dataframe
  509. filtered_df[f'lfp_ecog_{group_name}_coh'] = np.nan
  510. for iloc, row in filtered_df.iterrows():
  511. # Collect the data across all lfp-ecog pairs for the given group
  512. ls = []
  513. for nm in lfp_ecog:
  514. val = row['static_coh'][nm[0], nm[1], group[0]:group[1]].mean(axis=0)
  515. ls.append(val)
  516. ls = np.array(ls)
  517. assert ls.shape[0] == len(lfp_ecog)
  518. filtered_df.at[iloc, f'lfp_ecog_{group_name}_coh'] = ls.mean()
  519. # ^ coarse spectral bands
  520. for group_name, group in coarse_groups_indices.items():
  521. # Add an empty column to the dataframe
  522. filtered_df[f'lfp_ecog_{group_name}_coh'] = np.nan
  523. for iloc, row in filtered_df.iterrows():
  524. # Collect the data across all lfp-ecog pairs for the given group
  525. ls = []
  526. for nm in lfp_ecog:
  527. val = row['static_coh'][nm[0], nm[1], group[0]:group[1]].mean(axis=0)
  528. ls.append(val)
  529. ls = np.array(ls)
  530. assert ls.shape[0] == len(lfp_ecog)
  531. filtered_df.at[iloc, f'lfp_ecog_{group_name}_coh'] = ls.mean()
  532. # %% # ^ SAVE THE UPDATED DATAFRAME
  533. # Save the final med on med off dataframe to a pickle file
  534. current_date = datetime.now().strftime('%A_%B_%d_%Y_%H_%M_%S')
  535. # filtered_df.to_pickle(f'{model_name}_med_on_med_off_{current_date}_threshed.pkl')
  536. filtered_df.to_pickle(f'{model_name}_awake_and_asleep_{current_date}_threshed.pkl')
  537. # Save the final med on med off dataframe to a pickle file
  538. # %% # ^ LOAD THE DATAFRAME
  539. # This frame contains the temporal properties, clinical scores, and the spectral properties
  540. # The spectral properties include both the static spectral properties and the HMM spectral properties
  541. ground_truth_states = 4
  542. model_name = "HMM_masterModelMonday_June_10_2024_08_34_15"
  543. # Add date and time to file creation
  544. # ? THESE ARE IN RECYCLE BIN
  545. # load_date = 'Tuesday_December_10_2024_13_00_57'
  546. # load_date = 'Monday_February_03_2025_11_10_48'
  547. # load_date = 'Wednesday_February_12_2025_12_13_44'
  548. # ? THIS IS THE LATEST AND CORRECT
  549. load_date = 'Thursday_February_13_2025_22_16_39'
  550. filtered_df = pd.read_pickle(f'{model_name}_med_on_med_off_{load_date}_threshed.pkl')
  551. # %% # ^ Create a new column in the filtered_df where the bradykinesia_transformed is shifted as follows:
  552. # ! CRITICAL:
  553. # ^ This next bradykinesia score calculation and assignment has to be done before any other filtering
  554. # ^ is applied. This is absolutely essential for correct temporal alignment.
  555. # Get unique subject IDs
  556. subject_ids = filtered_df['subjectID'].unique()
  557. # The current row will have the next row's bradykinesia_transformed value and the last row will have no value
  558. # This has to be performed within each subject and not across subjects
  559. filtered_df = filtered_df.assign(next_bradykinesia_transformed=np.nan)
  560. # Rearrange the dataframe columns so that bradykinesia_transformed and next_bradykinesia_transformed are next to each other
  561. # All other columns should remain where they are
  562. # Get the column names
  563. cols = filtered_df.columns.tolist()
  564. # Rearrange the columns
  565. cols = cols[:cols.index('bradykinesia_transformed') + 1] + cols[-1:] + cols[cols.index('bradykinesia_transformed') + 1:-1]
  566. # Now rearrange the columns
  567. filtered_df = filtered_df[cols]
  568. for subject in subject_ids:
  569. # For this subject get the file numbers
  570. file_numbers = filtered_df[filtered_df['subjectID'] == subject]['file_number'].unique()
  571. # This ensures that the next bradykinesia scores are found within the same file number which is essentially the same session
  572. for file_number in file_numbers:
  573. # Get the indices of the subject and file number
  574. indices = filtered_df[(filtered_df['subjectID'] == subject) & (filtered_df['file_number'] == file_number)].index
  575. # Shift the values
  576. filtered_df.loc[indices, 'next_bradykinesia_transformed'] = filtered_df.loc[indices, 'bradykinesia_transformed'].shift(-1)
  577. # Last row should have no value assert this
  578. assert np.isnan(filtered_df.loc[indices[-1], 'next_bradykinesia_transformed'])
  579. # Drop the rows where the next_bradykinesia_transformed is nan
  580. filtered_df = filtered_df.dropna(subset=['next_bradykinesia_transformed'])
  581. # %% # ^ FILTER THE DATAFRAME
  582. filtered_df = filtered_df[filtered_df['bradykinesia_transformed'] < 80]
  583. # ! do not use this filter because med_on classification is not reliable
  584. # filtered_df = filtered_df[filtered_df['med_status'].isin(['med_off', 'med_on'])]
  585. # ! THIS IS THE MOST APPROPRIATE FILTER BECAUSE MED ON CLASSIFICATION IS NOT RELIABLE
  586. filtered_df = filtered_df[filtered_df['med_status'].isin(['med_off'])]
  587. # dyskinesia filter: behavior_status_dysk is either 'no-result' or 'non-dyskinetic'
  588. filtered_df = filtered_df[filtered_df['behavior_status_dysk'].isin(['no-result', 'non-dyskinetic'])]
  589. # Get unique subject IDs
  590. subject_ids = filtered_df['subjectID'].unique()
  591. # %%
  592. # & STATISTICAL ANALYSIS
  593. # ~ --------------------------------------------------------------------------------
  594. # ^ ---------------------------------ANALYSIS------------------------------------- ^
  595. # ~ --------------------------------------------------------------------------------
  596. # %%
  597. def benjamini_hochberg(p_values, alpha):
  598. """
  599. Apply the Benjamini-Hochberg procedure to a list of p-values.
  600. Parameters:
  601. p_values (list or np.array): List or array of p-values to be corrected.
  602. alpha (float): Desired false discovery rate (e.g., 0.05).
  603. Returns:
  604. np.array: Adjusted p-values.
  605. """
  606. # Convert p_values to a numpy array for convenience
  607. p_values = np.array(p_values)
  608. m = len(p_values)
  609. # Sort p-values and get the sorted indices
  610. sorted_indices = np.argsort(p_values)
  611. sorted_p_values = p_values[sorted_indices]
  612. # Compute critical values
  613. critical_values = (np.arange(1, m + 1) / m) * alpha
  614. # Find the largest k where p-value <= critical value
  615. below_critical = sorted_p_values <= critical_values
  616. if np.any(below_critical):
  617. k = np.max(np.where(below_critical)[0]) + 1
  618. else:
  619. k = 0
  620. # Compute adjusted p-values
  621. adjusted_p_values = np.minimum(1, (p_values * m) / (np.arange(1, m + 1)))
  622. # Sort adjusted p-values according to their original order
  623. final_p_values = np.ones_like(p_values)
  624. if k > 0:
  625. final_p_values[sorted_indices[:k]] = adjusted_p_values[sorted_indices[:k]]
  626. return final_p_values
  627. # %% # ^ GLM analysis for behavioral scores as dependent variables
  628. def glm_analysis_med_status_full(df, clinical_score, temporal_property,
  629. states,include_temporal_properties=True,
  630. link_function='log',
  631. glm_family = sm.families.Gamma(),
  632. include_spectral_properties=False,
  633. spectral_properties=None,
  634. include_subjectID=False,
  635. within_subject_centering=False,
  636. subjectID=None,
  637. scale_dependent_variable=False,
  638. write_to_file=False, file_name=None):
  639. results = {}
  640. # Filter data for a specific subject if subject is not none
  641. if subjectID and not include_subjectID:
  642. df = df[df['subjectID'] == subjectID]
  643. elif subjectID and include_subjectID:
  644. raise ValueError('subjectID should not be included as a covariate when a single subject is being analyzed')
  645. if include_temporal_properties is False and include_spectral_properties is False:
  646. raise ValueError('At least one of include_temporal_properties or include_spectral_properties must be True')
  647. if within_subject_centering and include_subjectID:
  648. raise ValueError('Within subject centering is not possible with subjectID included as a covariate\n'
  649. 'Use only one of within_subject_centering or include_subjectID')
  650. if include_temporal_properties:
  651. # Directly create the X dataframe with the temporal property for all the states
  652. X = df[[f'{temporal_property}_{s}' for s in states]]
  653. # Center the temporal property within each subject
  654. if within_subject_centering:
  655. X = X - df.groupby('subjectID')[X.columns].transform('mean')
  656. # Scale the temporal property between 0 and 1 for all states individually
  657. for s in states:
  658. X[f'{temporal_property}_{s}'] = (X[f'{temporal_property}_{s}'] - X[f'{temporal_property}_{s}'].min()) / (X[f'{temporal_property}_{s}'].max() - X[f'{temporal_property}_{s}'].min())
  659. # Add static spectral properties as independent variables if needed
  660. if include_spectral_properties:
  661. if include_temporal_properties:
  662. X = pd.concat([X, df[spectral_properties]], axis=1)
  663. # For each static spectral property, scale between 0 and 1
  664. for prop in spectral_properties:
  665. if within_subject_centering:
  666. X[prop] = X[prop] - df.groupby('subjectID')[prop].transform('mean')
  667. # Global scaling for stable model fitting
  668. X[prop] = (X[prop] - X[prop].min()) / (X[prop].max() - X[prop].min())
  669. else:
  670. X = df[spectral_properties]
  671. for prop in spectral_properties:
  672. if within_subject_centering:
  673. X[prop] = X[prop] - df.groupby('subjectID')[prop].transform('mean')
  674. # Global scaling for stable model fitting
  675. X[prop] = (X[prop] - X[prop].min()) / (X[prop].max() - X[prop].min())
  676. if include_subjectID:
  677. # ! Always drop the first column to avoid multicollinearity !!!
  678. X = pd.concat([X, pd.get_dummies(df['subjectID'], drop_first=True, dtype = 'int8')], axis=1)
  679. # ! Always drop the first column to avoid multicollinearity !!!
  680. # ! BUT DO NOT DROP ANY ROWS FROM X BASED ON NAN VALUES !!
  681. # ! THERE SHOULD BE NO NAN VALUES IN X !!
  682. y = df.loc[X.index, clinical_score].dropna()
  683. # Scale the clinical score between 0 and 1
  684. if scale_dependent_variable:
  685. y = (y - y.min()) / (y.max() - y.min())
  686. # Ensure X and y are aligned
  687. X = X.loc[y.index]
  688. # Add a constant term for the intercept
  689. X = sm.add_constant(X)
  690. # Ensure all columns in X are numeric
  691. X = X.apply(pd.to_numeric, errors='coerce')
  692. # Ensure y is numeric
  693. y = pd.to_numeric(y, errors='coerce')
  694. # Define the Tweedie family and GLM
  695. if link_function == 'log':
  696. link = sm.families.links.Log()
  697. elif link_function == 'identity':
  698. link = sm.families.links.Identity()
  699. glm_model = sm.GLM(y, X, family=glm_family, link=link)
  700. glm_results = glm_model.fit(cov_type='HC3', use_t=True)
  701. # Predict the clinical score using the model
  702. y_pred = glm_results.predict(X)
  703. # Add subject ID to the predictions
  704. y_pred = pd.concat([y_pred, df.loc[X.index, 'subjectID']], axis=1)
  705. # Add the actual clinical score to the predictions
  706. y_pred = pd.concat([y_pred, y], axis=1)
  707. # Name the columns
  708. y_pred.columns = ['Predicted', 'subjectID', 'Actual']
  709. return results, glm_results, glm_model, y_pred
  710. # return glm_results
  711. # %% # ^ FEATURES TO INCLUDE IN THE ANALYSIS
  712. # property_to_analyze = 'switching_rate'
  713. # property_to_analyze = 'FO'
  714. property_to_analyze = 'mean_lifetimes'
  715. # property_to_analyze = 'mean_intervals'
  716. # %%
  717. gaussian_family = sm.families.Gaussian(link=sm.families.links.Identity())
  718. # %% # ^ Independent variables 2
  719. # ^ Only unprojects properties: stn and ecog psds and lfp-ecog coherence
  720. STN_psd_props = []
  721. STN_psd_props = ['state_1_delta_alpha_lfp_psd_unprojected', 'state_2_delta_alpha_lfp_psd_unprojected',
  722. 'state_3_delta_alpha_lfp_psd_unprojected', 'state_4_delta_alpha_lfp_psd_unprojected',
  723. 'state_1_low_beta_lfp_psd_unprojected', 'state_2_low_beta_lfp_psd_unprojected',
  724. 'state_3_low_beta_lfp_psd_unprojected', 'state_4_low_beta_lfp_psd_unprojected',
  725. 'state_1_high_beta_lfp_psd_unprojected', 'state_2_high_beta_lfp_psd_unprojected',
  726. 'state_3_high_beta_lfp_psd_unprojected', 'state_4_high_beta_lfp_psd_unprojected',
  727. 'state_1_low_gamma_lfp_psd_unprojected', 'state_2_low_gamma_lfp_psd_unprojected',
  728. 'state_3_low_gamma_lfp_psd_unprojected', 'state_4_low_gamma_lfp_psd_unprojected',
  729. 'state_1_high_gamma_lfp_psd_unprojected', 'state_2_high_gamma_lfp_psd_unprojected',
  730. 'state_3_high_gamma_lfp_psd_unprojected', 'state_4_high_gamma_lfp_psd_unprojected',]
  731. ECOG_psd_props = []
  732. ECOG_psd_props = ['state_1_delta_alpha_ecog_psd_unprojected', 'state_2_delta_alpha_ecog_psd_unprojected',
  733. 'state_3_delta_alpha_ecog_psd_unprojected', 'state_4_delta_alpha_ecog_psd_unprojected',
  734. 'state_1_low_beta_ecog_psd_unprojected', 'state_2_low_beta_ecog_psd_unprojected',
  735. 'state_3_low_beta_ecog_psd_unprojected', 'state_4_low_beta_ecog_psd_unprojected',
  736. 'state_1_high_beta_ecog_psd_unprojected', 'state_2_high_beta_ecog_psd_unprojected',
  737. 'state_3_high_beta_ecog_psd_unprojected', 'state_4_high_beta_ecog_psd_unprojected',
  738. 'state_1_low_gamma_ecog_psd_unprojected', 'state_2_low_gamma_ecog_psd_unprojected',
  739. 'state_3_low_gamma_ecog_psd_unprojected', 'state_4_low_gamma_ecog_psd_unprojected',
  740. 'state_1_high_gamma_ecog_psd_unprojected', 'state_2_high_gamma_ecog_psd_unprojected',
  741. 'state_3_high_gamma_ecog_psd_unprojected', 'state_4_high_gamma_ecog_psd_unprojected',]
  742. LFP_ECOG_coherence_props = []
  743. LFP_ECOG_coherence_props = ['state_1_delta_alpha_lfp_ecog_coherence_unprojected', 'state_2_delta_alpha_lfp_ecog_coherence_unprojected',
  744. 'state_3_delta_alpha_lfp_ecog_coherence_unprojected', 'state_4_delta_alpha_lfp_ecog_coherence_unprojected',
  745. 'state_1_low_beta_lfp_ecog_coherence_unprojected', 'state_2_low_beta_lfp_ecog_coherence_unprojected',
  746. 'state_3_low_beta_lfp_ecog_coherence_unprojected', 'state_4_low_beta_lfp_ecog_coherence_unprojected',
  747. 'state_1_high_beta_lfp_ecog_coherence_unprojected', 'state_2_high_beta_lfp_ecog_coherence_unprojected',
  748. 'state_3_high_beta_lfp_ecog_coherence_unprojected', 'state_4_high_beta_lfp_ecog_coherence_unprojected',
  749. 'state_1_low_gamma_lfp_ecog_coherence_unprojected', 'state_2_low_gamma_lfp_ecog_coherence_unprojected',
  750. 'state_3_low_gamma_lfp_ecog_coherence_unprojected', 'state_4_low_gamma_lfp_ecog_coherence_unprojected',
  751. 'state_1_high_gamma_lfp_ecog_coherence_unprojected', 'state_2_high_gamma_lfp_ecog_coherence_unprojected',
  752. 'state_3_high_gamma_lfp_ecog_coherence_unprojected', 'state_4_high_gamma_lfp_ecog_coherence_unprojected']
  753. # Combine all the unprojected properties in a single list
  754. final_spectral_props = (STN_psd_props + ECOG_psd_props + LFP_ECOG_coherence_props)
  755. # %% # ^ Independent variables 3: Only static spectral props > local psds and coherence
  756. static_spectral_properties = ['lfp_low_beta_psd', 'ecog_low_beta_psd',
  757. 'lfp_high_beta_psd', 'ecog_high_beta_psd',
  758. 'lfp_low_gamma_psd', 'ecog_low_gamma_psd',
  759. 'lfp_high_gamma_psd', 'ecog_high_gamma_psd',
  760. 'lfp_delta_alpha_psd', 'ecog_delta_alpha_psd']
  761. static_lfp_ecog_coherence = ['lfp_ecog_delta_alpha_coh',
  762. 'lfp_ecog_low_beta_coh',
  763. 'lfp_ecog_high_beta_coh',
  764. 'lfp_ecog_low_gamma_coh',
  765. 'lfp_ecog_high_gamma_coh',]
  766. final_spectral_props = (static_spectral_properties + static_lfp_ecog_coherence)
  767. # %% # ^ Check if the final_spectral_props are in the dataframe
  768. # Check if final_spectral_props are in the dataframe
  769. for prop in final_spectral_props:
  770. if prop not in filtered_df.columns:
  771. print(f'{prop} not in the dataframe')
  772. # %%
  773. # & GLM Analysis
  774. single_subjectID = False
  775. if single_subjectID:
  776. single_subjectID_name = 'RCS05L'
  777. include_subjectID = False
  778. else:
  779. single_subjectID_name = None
  780. include_subjectID = True
  781. # ^ For FO, we have to use one state at a time due to the compositional nature of FO
  782. # ^ Refer to compositional analysis for more details (hint: FO sums to 1)
  783. include_temporal_properties = False
  784. include_spectral_properties = True
  785. if property_to_analyze == 'FO' and include_temporal_properties:
  786. states = [1] # Change this to the state you want to analyze, repeat for each state
  787. else:
  788. states = [1, 2, 3, 4]
  789. _, results, model, y_pred = glm_analysis_med_status_full(filtered_df,
  790. 'next_bradykinesia_transformed',
  791. temporal_property=property_to_analyze,
  792. states = states,
  793. include_temporal_properties=include_temporal_properties,
  794. glm_family=gaussian_family,
  795. link_function='identity',
  796. include_spectral_properties = include_spectral_properties,
  797. include_subjectID=include_subjectID,
  798. within_subject_centering=False,
  799. subjectID=single_subjectID_name,
  800. spectral_properties=final_spectral_props,
  801. scale_dependent_variable=True,
  802. write_to_file=False)
  803. print(results.summary())
  804. # Get the degrees of freedom for residuals , the model and the number of observations from the results
  805. results.df_resid
  806. results.df_model
  807. results.nobs
  808. # %% # ^ TEMPORAL PROPERTY SIMPLE OLS MODELS
  809. dependent_variable_name = 'next_bradykinesia_transformed'
  810. # independent_variable = ['FO_2', 'FO_4']
  811. # independent_variable = [ 'mean_lifetimes_2', 'mean_lifetimes_4']
  812. # independent_variable = ['mean_lifetimes_1', 'mean_lifetimes_2', 'mean_lifetimes_3' ,'mean_lifetimes_4']
  813. # independent_variable = ['switching_rate_1', 'switching_rate_2', 'switching_rate_3', 'switching_rate_4']
  814. # independent_variable = ['FO_1', 'FO_2', 'FO_3', 'FO_4']
  815. independent_variable = ['mean_intervals_1', 'mean_intervals_2', 'mean_intervals_3', 'mean_intervals_4']
  816. single_subjectID_name = 'RCS08R'
  817. single_subjectID = True
  818. # Simple model
  819. subject_filtered_df = filtered_df[filtered_df['subjectID'] == single_subjectID_name]
  820. independent_variable = subject_filtered_df[independent_variable]
  821. # Add a constant term for the intercept
  822. independent_variable = sm.add_constant(independent_variable)
  823. dependent_variable = subject_filtered_df[dependent_variable_name]
  824. # Scale the dependent variable between 0 and 1 # This is optional t-values are not affected by scaling
  825. max_dep = np.max(dependent_variable)
  826. min_dep = np.min(dependent_variable)
  827. dependent_variable = (dependent_variable - min_dep)/(max_dep-min_dep)
  828. # Fit a simple linear regression model
  829. model = sm.OLS(dependent_variable, independent_variable)
  830. results = model.fit(cov_type='HC3', use_t=True)
  831. # display the results
  832. print(results.summary())
  833. # %% # ^ SPECTRAL PROPERTY
  834. # Calculate the R2 value for the model based on the y_pred
  835. # calculate the R2 value
  836. R2 = 1 - np.sum((y_pred['Actual'] - y_pred['Predicted'])**2) / np.sum((y_pred['Actual'] - np.mean(y_pred['Actual']))**2)
  837. print('The R2 value is: ' + str(R2))
  838. # number of observations
  839. n = len(y_pred)
  840. # Get the number of independent variables
  841. p = len(model.exog_names) - 1
  842. # calculate the adjusted R2 value
  843. adj_R2 = 1 - (1 - R2) * (n - 1) / (n - p - 1)
  844. print('The adjusted R2 value is: ' + str(adj_R2))
  845. # %% # ^ SPECTRAL PROPERTY Let us plot the predicted vs actual clinical score using the y pred dataframe
  846. # Get the unique subject IDs
  847. subject_ids = y_pred['subjectID'].unique()
  848. # Cycle through the subject IDs and plot the predicted vs actual clinical score along with the best fit line
  849. # We will use a unique color for each subject ID
  850. # Assign a unique color to each subject ID
  851. colors = sns.color_palette('husl', n_colors=len(subject_ids))
  852. for i, subject_id in enumerate(subject_ids):
  853. # Get the data for the current subject ID
  854. curr_data = y_pred[y_pred['subjectID'] == subject_id]
  855. # Create a new figure for each subject ID
  856. plt.figure(figsize=(10, 6))
  857. # Fit a best fit line to the plot
  858. sns.regplot(data=curr_data, x='Actual',
  859. y='Predicted', lowess=True, color=colors[i],
  860. scatter_kws={'color': colors[i], 'alpha': 0.1, 's': 50},
  861. line_kws={'color': 'black', 'linewidth': 2})
  862. # Set the title for the plot
  863. plt.title(f'Subject ID: {subject_id}')
  864. # Show the plot
  865. # plt.show()
  866. # %% # ^ SPECTRAL PROPERTY DATAFRAMES
  867. # & Extract the results
  868. all_spectral_bands = ['high_gamma', 'low_gamma', 'high_beta', 'low_beta', 'delta_alpha']
  869. all_channel_types_psd = ['lfp_psd', 'ecog_psd', 'lfp_ecog']
  870. coef_df = pd.DataFrame({
  871. "Coefficient": results.params,
  872. "Standard Error": results.bse,
  873. "t-value": results.tvalues,
  874. "p-value": results.pvalues,
  875. "Confidence Interval (0.025)": results.conf_int()[0],
  876. "Confidence Interval (0.975)": results.conf_int()[1]
  877. })
  878. significant_results = deepcopy(coef_df[coef_df['p-value'] < 1.00])
  879. significant_results = significant_results.assign(State=None, Spectral_Band=None, Channel_Type=None)
  880. for index, row in significant_results.iterrows():
  881. # Split the predictor name
  882. predictor_name = index
  883. # Split the predictor name
  884. split_name = predictor_name.split('_')
  885. # Check if the predictor name contains the state
  886. if 'state' in split_name[0]:
  887. state = split_name[0] + '_' + split_name[1]
  888. # Check if the predictor name contains the spectral band
  889. spectral_band = None
  890. channel_type = None
  891. if 'coh' in split_name[-1]:
  892. spectral_band = split_name[2] + '_' + split_name[3]
  893. channel_type = split_name[4] + '_' + split_name[5]
  894. if 'coh' in split_name and 'unprojected' in split_name:
  895. spectral_band = split_name[2] + '_' + split_name[3]
  896. channel_type = split_name[4] + '_' + split_name[5] # dont add unprojected
  897. if 'coherence' in split_name and 'unprojected' in split_name:
  898. spectral_band = split_name[2] + '_' + split_name[3]
  899. channel_type = split_name[4] + '_' + split_name[5]
  900. if 'psd' in split_name[-1]:
  901. spectral_band = split_name[2] + '_' + split_name[3]
  902. channel_type = split_name[4] + '_' + split_name[5]
  903. if 'psd' in split_name and 'unprojected' in split_name:
  904. spectral_band = split_name[2] + '_' + split_name[3]
  905. channel_type = split_name[4] + '_' + split_name[5] # dont add unprojected
  906. else:
  907. state = 'None'
  908. spectral_band = 'None'
  909. channel_type = 'None'
  910. if 'lfp' in split_name and 'ecog' in split_name:
  911. # If the predictor name contains lfp and ecog, we will set the channel type to lfp_ecog
  912. channel_type = 'lfp_ecog'
  913. # Remove lfp, ecog and coh/coherence from the split name
  914. split_name = [name for name in split_name if name not in ['lfp', 'ecog', 'coh', 'coherence', 'psd', 'unprojected']]
  915. # Now the join the remaining parts of the split name to get the spectral band
  916. if len(split_name) > 1:
  917. spectral_band = '_'.join(split_name[0:])
  918. elif 'lfp' in split_name and 'psd' in split_name:
  919. channel_type = 'lfp_psd'
  920. # Remove lfp and psd from the split name
  921. split_name = [name for name in split_name if name not in ['lfp', 'psd', 'unprojected']]
  922. # Now the join the remaining parts of the split name to get the spectral band
  923. if len(split_name) > 1:
  924. spectral_band = '_'.join(split_name[0:])
  925. elif 'ecog' in split_name and 'psd' in split_name:
  926. channel_type = 'ecog_psd'
  927. # Remove ecog and psd from the split name
  928. split_name = [name for name in split_name if name not in ['ecog', 'psd', 'unprojected']]
  929. # Now the join the remaining parts of the split name to get the spectral band
  930. if len(split_name) > 1:
  931. spectral_band = '_'.join(split_name[0:])
  932. # Update the dataframe
  933. significant_results.at[index, 'State'] = state
  934. significant_results.at[index, 'Spectral Band'] = spectral_band
  935. significant_results.at[index, 'Channel Type'] = channel_type
  936. # Drop the following columns from the significant results
  937. # significant_results.drop(columns=['Standard Error',
  938. # 'Confidence Interval (0.025)',
  939. # 'Confidence Interval (0.975)'],
  940. # inplace=True)
  941. # # Reorder the columns
  942. significant_results = significant_results[[ 'State', 'Spectral Band', 'Channel Type',
  943. 'Coefficient','t-value', 'p-value', 'Standard Error',
  944. 'Confidence Interval (0.025)',
  945. 'Confidence Interval (0.975)']]
  946. significant_results = significant_results[
  947. (significant_results['Spectral Band'] != 'None') &
  948. (significant_results['Channel Type'] != 'None')]
  949. if single_subjectID:
  950. # Add a column for the subject ID in the significant results dataframe for all rows
  951. significant_results['Subject ID'] = single_subjectID_name
  952. # ^ ULTRA CORRECTION: Correcting across states, spectral bands and channel types
  953. # # Filter the results based on the corrected p-values
  954. # significant_results_new = significant_results[significant_results['p-value_corr'] < 0.001]
  955. if not single_subjectID:
  956. # Correcting every p-value found
  957. # Correct the p-values using fdr
  958. p_values = significant_results['p-value']
  959. p_values_corrected = multipletests(p_values, method='fdr_bh', alpha = 0.05)[1]
  960. # Update the p-values in the dataframe
  961. significant_results['p-value_corr'] = p_values_corrected
  962. # Add a new column called: corrected_significance: True if the corrected p-value is less than 0.01
  963. significant_results['corrected_significance'] = significant_results['p-value_corr'] < 0.01
  964. # Add a new row at the end of the dataframe
  965. significant_results.loc[len(significant_results)] = {
  966. 'State': 'DF_residuals',
  967. 'Spectral Band': results.df_resid,
  968. 'Channel Type': 'DF_model',
  969. 'Coefficient': results.df_model,
  970. 't-value': 'Num obs',
  971. 'p-value': results.nobs,
  972. 'Standard Error': 0,
  973. 'Confidence Interval (0.025)': 0,
  974. 'Confidence Interval (0.975)': 0
  975. }
  976. # Filter the results based on the corrected p-values
  977. significant_results_ULTRA_CORRECTED = significant_results[significant_results['corrected_significance']]
  978. # Add a new row at the end of the dataframe
  979. significant_results_ULTRA_CORRECTED.loc[len(significant_results_ULTRA_CORRECTED)] = {
  980. 'State': 'DF_residuals',
  981. 'Spectral Band': results.df_resid,
  982. 'Channel Type': 'DF_model',
  983. 'Coefficient': results.df_model,
  984. 't-value': 'Num obs',
  985. 'p-value': results.nobs,
  986. 'Standard Error': 0,
  987. 'Confidence Interval (0.025)': 0,
  988. 'Confidence Interval (0.975)': 0
  989. }
  990. # %% # ^ SPECTRAL PROPERTIES ULTRA CORRECTION PLOTS
  991. posthoc_ULTRA_glm_figure_dir = r'C:\Oxford\software\wireless\results\panel_figures\posthocs-ULTRA_NEWNEW'
  992. spectral_band_colors = {
  993. 'delta_alpha': '#00A36C', # green
  994. 'low_beta': '#FFB336', # ligher yellow
  995. 'high_beta': '#E18500', # darker yellow
  996. 'low_gamma': '#15A8FF', # light blue
  997. 'high_gamma': '#004FB4' # dark blue
  998. }
  999. spectral_band_tuple_dict = {
  1000. 'delta_alpha': (1.8, 1.9),
  1001. 'low_beta': (1.4, 1.5),
  1002. 'high_beta': (1.0, 1.1),
  1003. 'low_gamma': (0.6, 0.7),
  1004. 'high_gamma': (0.2, 0.3)
  1005. }
  1006. all_spectral_bands = ['high_gamma', 'low_gamma', 'high_beta', 'low_beta', 'delta_alpha']
  1007. all_spectral_band_names = ['high gamma', 'low gamma', 'high beta', 'low beta', 'delta/alpha']
  1008. bar_height = 0.2
  1009. bar_offset = 0.2 # Offset to ensure consistent spacing between positive and negative bars
  1010. left = 0
  1011. bar_alpha = 0.9
  1012. global_min = -15.0
  1013. global_max = 15.0
  1014. # Add stars based on siginificance level
  1015. # if p_values_corrected < 0.001:
  1016. # stars = '***'
  1017. # if p_values_corrected < 0.01:
  1018. # stars = '**'
  1019. # if p_values_corrected < 0.05:
  1020. # stars = '*'
  1021. for st in significant_results['State'].unique():
  1022. for ch in significant_results['Channel Type'].unique():
  1023. st_ch_data = significant_results[(significant_results['State'] == st) &
  1024. (significant_results['Channel Type'] == ch)]
  1025. fig, ax = plt.subplots(figsize=(6, 4))
  1026. ax.axvline(x=0, color='black', linewidth=1)
  1027. ax.set_xlim(global_min, global_max)
  1028. for i, row in st_ch_data.iterrows():
  1029. row_tval = row['t-value']
  1030. corrected_significance = row['corrected_significance']
  1031. if corrected_significance:
  1032. bar_color = spectral_band_colors[row['Spectral Band']]
  1033. else:
  1034. bar_color = 'darkgrey'
  1035. y_tuple = spectral_band_tuple_dict[row['Spectral Band']]
  1036. if row_tval > 0:
  1037. ax.barh(
  1038. y=y_tuple[0],
  1039. width=row_tval,
  1040. xerr=row['Standard Error'],
  1041. color=bar_color,
  1042. edgecolor='black',
  1043. left=left,
  1044. height=bar_height,
  1045. alpha=bar_alpha
  1046. )
  1047. ax.barh(
  1048. y=y_tuple[1],
  1049. width=-0.1,
  1050. xerr=None,
  1051. color='white',
  1052. edgecolor=bar_color,
  1053. left=left,
  1054. height=bar_height,
  1055. alpha=bar_alpha
  1056. )
  1057. if row_tval < 0:
  1058. ax.barh(
  1059. y=y_tuple[0],
  1060. width=0.1,
  1061. xerr=None,
  1062. color='white',
  1063. edgecolor=bar_color,
  1064. left=left,
  1065. height=bar_height,
  1066. alpha=bar_alpha
  1067. )
  1068. ax.barh(
  1069. y=y_tuple[1],
  1070. width=row_tval,
  1071. xerr=row['Standard Error'],
  1072. color=bar_color,
  1073. edgecolor='black',
  1074. left=left,
  1075. height=bar_height,
  1076. alpha=bar_alpha
  1077. )
  1078. # Add stars for significance
  1079. if row['p-value_corr'] < 0.001:
  1080. stars = '***'
  1081. elif row['p-value_corr'] < 0.01:
  1082. stars = '**'
  1083. elif row['p-value_corr'] < 0.05:
  1084. stars = '*'
  1085. else:
  1086. stars = ''
  1087. if stars:
  1088. ax.text(
  1089. x=row_tval + (0.5 if row_tval > 0 else -0.5), # Adjust position based on bar direction
  1090. y=y_tuple[0] + bar_height / 2,
  1091. s=stars,
  1092. fontsize=30,
  1093. ha='center',
  1094. va='center',
  1095. color='black'
  1096. )
  1097. # Set y tick labels as the spectral bands
  1098. ax.set_yticks([0.25, 0.65, 1.05, 1.45, 1.85])
  1099. ax.set_yticklabels(all_spectral_band_names, fontsize=16)
  1100. ax.set_xticks([-10, 0, 10])
  1101. ax.set_xlabel('t-value', fontsize=16, fontname='Arial')
  1102. ax.set_ylabel('Spectral Band', fontsize=16, fontname='Arial')
  1103. ax.tick_params(axis='x', labelsize=18)
  1104. ax.tick_params(axis='y', labelsize=18)
  1105. # Increase the size of the actual ticks and make them inward
  1106. ax.tick_params(axis='both', which='major', length=15, direction='in')
  1107. # Switch off the frame
  1108. ax.grid(True, axis='x', linestyle='-', alpha=1.0)
  1109. ax.grid(True, axis='y', linestyle='--', alpha=0.7)
  1110. # Set the title
  1111. ax.set_title(f'State {st} {ch}', fontsize=18, fontname='Arial', pad=20)
  1112. plt.show()
  1113. # Save the figure
  1114. fig.savefig(os.path.join(posthoc_ULTRA_glm_figure_dir, f'STARRED_ULTRA_{st}_{ch}.png'), dpi=600, bbox_inches='tight')
  1115. # %% # ^ TEMPORAL PROPERTY DATAFRAMES
  1116. coef_df = pd.DataFrame({
  1117. "Coefficient": results.params,
  1118. "Standard Error": results.bse,
  1119. "t-value": results.tvalues,
  1120. "p-value": results.pvalues,
  1121. "Confidence Interval (0.025)": results.conf_int()[0],
  1122. "Confidence Interval (0.975)": results.conf_int()[1]
  1123. })
  1124. significant_results = deepcopy(coef_df[coef_df['p-value'] < 1.00])
  1125. significant_results = significant_results.assign(State=None, Temporal_Property=None)
  1126. for index, row in significant_results.iterrows():
  1127. # Split the predictor name
  1128. predictor_name = index
  1129. # Split the predictor name
  1130. split_name = predictor_name.split('_')
  1131. if len(split_name) == 2:
  1132. significant_results.at[index, 'State'] = split_name[1]
  1133. significant_results.at[index, 'Temporal_Property'] = split_name[0]
  1134. elif len(split_name) == 3:
  1135. significant_results.at[index, 'State'] = split_name[2]
  1136. significant_results.at[index, 'Temporal_Property'] = split_name[0] + '_' + split_name[1]
  1137. else:
  1138. significant_results.at[index, 'State'] = 'None'
  1139. significant_results.at[index, 'Temporal_Property'] = 'None'
  1140. significant_results = significant_results[[ 'State', 'Temporal_Property',
  1141. 'Coefficient','t-value', 'p-value', 'Standard Error',
  1142. 'Confidence Interval (0.025)',
  1143. 'Confidence Interval (0.975)']]
  1144. significant_results = significant_results[(significant_results['State'] != 'None')]
  1145. if single_subjectID:
  1146. # Add a column for the subject ID in the significant results dataframe for all rows
  1147. significant_results['Subject ID'] = single_subjectID_name
  1148. # ^ ULTRA CORRECTION: For a specific temporal property, correcting across all states
  1149. # ^ This is not applicable to Fractional Occupancy (FO): For FO correct manually
  1150. if not single_subjectID and len(states) > 1:
  1151. # Correcting every p-value found
  1152. # Correct the p-values using fdr
  1153. p_values = significant_results['p-value']
  1154. p_values_corrected = multipletests(p_values, method='fdr_tsbh', alpha = 0.00001)[1]
  1155. # Update the p-values in the dataframe
  1156. significant_results['p-value_corr'] = p_values_corrected
  1157. # Add a new column called: corrected_significance: True if the corrected p-value is less than 0.01
  1158. significant_results['corrected_significance'] = significant_results['p-value_corr'] < 0.01
  1159. if property_to_analyze == 'FO':
  1160. # Prompt the user to enter 4 p-values for the states
  1161. p_values = []
  1162. for state in range(1, 5):
  1163. while True:
  1164. try:
  1165. p_value = float(input(f"Enter the p-value for State {state}: "))
  1166. if 0 <= p_value <= 1:
  1167. p_values.append(p_value)
  1168. break
  1169. else:
  1170. print("Please enter a valid p-value between 0 and 1.")
  1171. except ValueError:
  1172. print("Invalid input. Please enter a numeric value.")
  1173. # Correct the p-values using the Benjamini-Hochberg procedure
  1174. alpha = 0.05
  1175. corrected_p_values = multipletests(p_values, method='fdr_bh', alpha=alpha)[1]
  1176. # Display the corrected p-values
  1177. for state, corrected_p in enumerate(corrected_p_values, start=1):
  1178. print(f"Corrected p-value for State {state}: {corrected_p}")
  1179. # %% # ^ ULTRA CORRECTION TEMPORAL PROPERTIES PLOTS
  1180. posthoc_ULTRA_glm_figure_dir = r'C:\Oxford\software\wireless\results\panel_figures\posthocs_temporal-ULTRA'
  1181. temporal_property = 'FO'
  1182. model_name = 'HMM_masterModelMonday_June_10_2024_08_34_15'
  1183. # Load the data
  1184. temporal_property_data_dir = r'C:\Oxford\software\wireless\results\HMM_masterModelMonday_June_10_2024_08_34_15\GLMs\Temporal_properties_May_2025'
  1185. file_name = f'None_{model_name}_ALLSTATES_MODEL_TEMPORAL_PROP_{temporal_property}_2025-05-14.csv'
  1186. # Load the data
  1187. temporal_property_data = pd.read_csv(os.path.join(temporal_property_data_dir, file_name))
  1188. # Get the unique states
  1189. unique_states = temporal_property_data['State'].unique()
  1190. # State colors = orange, blue, green, lilac
  1191. state_colors = {
  1192. 1: '#FFB336', # orange
  1193. 2: '#004FB4', # blue
  1194. 3: '#00A36C', # green
  1195. 4: '#E18500' # lilac
  1196. }
  1197. state_tuple_dict = {
  1198. 1: (1.8, 1.9),
  1199. 2: (1.4, 1.5),
  1200. 3: (1.0, 1.1),
  1201. 4: (0.6, 0.7)
  1202. }
  1203. bar_height = 0.2
  1204. bar_offset = 0.2 # Offset to ensure consistent spacing between positive and negative bars
  1205. left = 0
  1206. bar_alpha = 0.9
  1207. global_min = -6.0
  1208. global_max = 6.0
  1209. all_state_names = ['4', '3', '2', '1']
  1210. fig, ax = plt.subplots(figsize=(6, 4))
  1211. for st in temporal_property_data['State'].unique():
  1212. st_ch_data = temporal_property_data[(temporal_property_data['State'] == st)]
  1213. ax.axvline(x=0, color='black', linewidth=1)
  1214. ax.set_xlim(global_min, global_max)
  1215. for i, row in st_ch_data.iterrows():
  1216. row_tval = row['t-value']
  1217. corrected_significance = row['corrected_significance']
  1218. if corrected_significance:
  1219. bar_color = state_colors[row['State']]
  1220. else:
  1221. bar_color = 'darkgrey'
  1222. y_tuple = state_tuple_dict[row['State']]
  1223. if row_tval > 0:
  1224. ax.barh(
  1225. y=y_tuple[0],
  1226. width=row_tval,
  1227. xerr=row['Standard Error'],
  1228. color=bar_color,
  1229. edgecolor='black',
  1230. left=left,
  1231. height=bar_height,
  1232. alpha=bar_alpha
  1233. )
  1234. ax.barh(
  1235. y=y_tuple[1],
  1236. width=-0.1,
  1237. xerr=None,
  1238. color='white',
  1239. edgecolor=bar_color,
  1240. left=left,
  1241. height=bar_height,
  1242. alpha=bar_alpha
  1243. )
  1244. if row_tval < 0:
  1245. ax.barh(
  1246. y=y_tuple[0],
  1247. width=0.1,
  1248. xerr=None,
  1249. color='white',
  1250. edgecolor=bar_color,
  1251. left=left,
  1252. height=bar_height,
  1253. alpha=bar_alpha
  1254. )
  1255. ax.barh(
  1256. y=y_tuple[1],
  1257. width=row_tval,
  1258. xerr=row['Standard Error'],
  1259. color=bar_color,
  1260. edgecolor='black',
  1261. left=left,
  1262. height=bar_height,
  1263. alpha=bar_alpha
  1264. )
  1265. # Add stars for significance
  1266. # if row['p-value_corr'] < 0.001:
  1267. # stars = '***'
  1268. # elif row['p-value_corr'] < 0.01:
  1269. # stars = '**'
  1270. # elif row['p-value_corr'] < 0.05:
  1271. # stars = '*'
  1272. # else:
  1273. # stars = ''
  1274. # if stars:
  1275. # ax.text(
  1276. # x=row_tval + (0.5 if row_tval > 0 else -0.5) if abs(row_tval) > 1 else row_tval + (0.2 if row_tval > 0 else -0.2), # Adjust position based on bar length
  1277. # y=y_tuple[0] + bar_height / 2,
  1278. # s=stars,
  1279. # fontsize=30,
  1280. # ha='center',
  1281. # va='center',
  1282. # color='black'
  1283. # )
  1284. # Set y tick labels as the spectral bands
  1285. ax.set_yticks([0.65, 1.05, 1.45, 1.85])
  1286. ax.set_yticklabels(all_state_names, fontsize=16)
  1287. ax.set_xticks([-6, 0, 6])
  1288. ax.set_xlabel('t-value', fontsize=16, fontname='Arial')
  1289. ax.set_ylabel('States', fontsize=20, fontname='Arial')
  1290. ax.tick_params(axis='x', labelsize=18)
  1291. ax.tick_params(axis='y', labelsize=18)
  1292. # Increase the size of the actual ticks and make them inward
  1293. ax.tick_params(axis='both', which='major', length=15, direction='in')
  1294. # Switch off the frame
  1295. ax.grid(True, axis='x', linestyle='-', alpha=1.0)
  1296. ax.grid(True, axis='y', linestyle='--', alpha=0.7)
  1297. # Set the title
  1298. # Change temporal property case to first letter capital
  1299. temporal_property = temporal_property.capitalize()
  1300. ax.set_title(f'{temporal_property}', fontsize=18, fontname='Arial', pad=20)
  1301. plt.show()
  1302. # Save the figure
  1303. posthoc_ULTRA_temporals = r'C:\Oxford\software\wireless\results\panel_figures\posthocs_temporal-ULTRA'
  1304. fig.savefig(os.path.join(posthoc_ULTRA_temporals, f'ULTRA_{temporal_property}.png'), dpi=600, bbox_inches='tight')
  1305. # %% # ^ FOR BOTH SPECTRAL AND TEMPORAL PROPERTIES
  1306. # Save the results to a csv file drop the index
  1307. # significant_results.to_csv(f'significant_glm_results_bradykinesia_HMM_coh_spectra_without_med_control_within_subjects.csv', index=False)
  1308. # Add date of creation to the file name
  1309. # Get the current date
  1310. current_date = datetime.now().strftime('%Y-%m-%d')
  1311. # Get current time in hours, minutes and seconds am or pm
  1312. # current_time = datetime.now().strftime('%I-%M-%p')
  1313. # Add the date and time to the file name
  1314. # ^ PLEASE CHECK THE FILE NAME AND DIRECTORY
  1315. # ! DO NOT OVERWRITE THE FILE NAME
  1316. # GLM_result_directory = r'C:\Oxford\software\wireless\results\HMM_masterModelMonday_June_10_2024_08_34_15\GLMs\Temporal_properties_May_2025'
  1317. # Ask the user to enter the directory
  1318. GLM_result_directory = input('Enter the directory to save the results: ')
  1319. # Check if the directory exists, if not create it
  1320. if not os.path.exists(GLM_result_directory):
  1321. os.makedirs(GLM_result_directory)
  1322. if not(property_to_analyze == 'FO') and include_temporal_properties:
  1323. significant_results.to_csv(os.path.join(GLM_result_directory,
  1324. f'{single_subjectID_name}_{model_name}_ALLSTATES_MODEL_TEMPORAL_PROP_INTERVALS_{current_date}.csv'),
  1325. index=False)
  1326. if property_to_analyze == 'FO' and include_temporal_properties:
  1327. significant_results.to_csv(os.path.join(GLM_result_directory,
  1328. f'{single_subjectID_name}_{model_name}_STATE_{states[0]}_MODEL_TEMPORAL_PROP_FO_{current_date}.csv'),
  1329. index=False)
  1330. if not include_temporal_properties:
  1331. significant_results.to_csv(os.path.join(GLM_result_directory,
  1332. f'{single_subjectID_name}_{model_name}_ALLSTATES_MODEL_SPECTRAL_PROP_{current_date}.csv'),
  1333. index=False)
  1334. # If the variable significant_results_ULTRA_CORRECTED exists, save it to a csv file
  1335. if 'significant_results_ULTRA_CORRECTED' in locals():
  1336. significant_results_ULTRA_CORRECTED.to_csv(os.path.join(GLM_result_directory,
  1337. f'{single_subjectID_name}_{model_name}_ALLSTATES_MODEL_SPECTRAL_PROP_ULTRA_CORRECTED_{current_date}.csv'),
  1338. index=False)
  1339. # %% # ^ Collect model information and diagnostics for GLM
  1340. #
  1341. model_info = {
  1342. "Dependent Variable": results.model.endog_names,
  1343. "Model Type": type(model).__name__,
  1344. "Family": results.family.__class__.__name__,
  1345. "Link Function": results.family.link.__class__.__name__,
  1346. "Observations": results.nobs,
  1347. "Degrees of Freedom (Residual)": results.df_resid,
  1348. "Degrees of Freedom (Model)": results.df_model,
  1349. "Pearson Chi2": results.pearson_chi2,
  1350. "Scale": results.scale,
  1351. "Log-Likelihood": results.llf,
  1352. "Deviance": results.deviance,
  1353. "Null Deviance": results.null_deviance,
  1354. "AIC": results.aic,
  1355. "BIC": results.bic,
  1356. "Pseudo R-squared (CS)": results.pseudo_rsquared() # Pseudo R-squared (only applicable for certain GLM types)
  1357. }
  1358. # Convert to DataFrame
  1359. model_info_df = pd.DataFrame(list(model_info.items()), columns=["Metric", "Value"])
  1360. # %% # ^ Collect model information and diagnostics for GLM
  1361. # Perform the Wald test for the model
  1362. wald_test = results.wald_test_terms()
  1363. # Extract the datafraem
  1364. wald_test_df = wald_test.summary_frame()
  1365. # Only extract the dataframe for results where p-value is less than 0.01
  1366. significant_wald_test = wald_test_df[wald_test_df['P>F'] < 0.01]
  1367. #%% # ^ Let us segegrate the significant wald test based on states
  1368. # Will use this to only evaluate results if spectral properties are being studied
  1369. significant_spectral_wald_state = {}
  1370. for state in [1, 2, 3, 4]:
  1371. state_wald_test = significant_wald_test[significant_wald_test.index.str.contains(f'{state}')]
  1372. curr_state = {}
  1373. for row in state_wald_test.iterrows():
  1374. # Get the row index
  1375. index = row[0]
  1376. # Get the coefficient value
  1377. curr_state[index] = significant_results.loc[index].loc['Coefficient']
  1378. significant_spectral_wald_state[f'State {state}'] = curr_state
  1379. # %%

eBioMed_postAnalysis_bradykinesia.py at commit b4b7158, under AGPL-3.0 · at the source

Overview

Authors: Abhinav Sharma1,2, Tao Liu1,2, Bahman Abdi-Sargezeh1,2, Amelia Hahn3, Maria Shcherbakova3, Wolf-Julian Neumann4, Simon Little3, Philip Starr3, Ashwini Oswal1,2
  1. Nuffield Department of Clinical Neurosciences, University of Oxford, Oxford, United Kingdom
  2. MRC Brain Networks Dynamics Unit, University of Oxford, Oxford, United Kingdom
  3. Weill Institute for Neurosciences, Department of Neurology, University of California San Francisco, San Francisco, CA, USA
  4. Movement Disorder and Neuromodulation Unit, Department of Neurology, Charité – Universitätsmedizin, Berlin, Germany
Journal: EBioMedicine, volume 128, article 106293
Dates: received 31 July 2025; accepted 28 April 2026; published online 12 May 2026; in print June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.ebiom.2026.106293 · PMID 42119302 · PMCID PMC13191631 · OpenAlex W7161000153
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), Parkinson's (population)
Methods: Spectral & time-frequency, Statistics, Connectivity, Machine learning, fMRI & imaging
Keywords: Neural states, Bradykinesia, Parkinson's disease
MeSH: Motor Cortex*, Parkinson Disease*, Subthalamic Nucleus*, Adult, Female, Humans, Hypokinesia, Longitudinal Studies, Male, Middle Aged, Severity of Illness Index, Tremor (* major topic)
Topic: Neurological disorders and treatments (Neurology, Medicine), according to OpenAlex
Citations: not cited yet (Europe PMC); 64 references in the paper

Abstract

Background: Motor symptoms in Parkinson's disease (PD) may arise due to transient, network-wide neural dynamics that extend beyond beta-band oscillatory activity within the motor cortical-subthalamic nucleus (STN) circuit.

Methods: We applied a four-state Hidden Markov Model (HMM) to identify states of local and interregional oscillatory synchrony from chronic motor cortical and STN recordings (1046 h from 10 hemispheres) in five patients with PD (mean age 49 years), with concurrent measurements of bradykinesia, dyskinesia and tremor quantified using wearable sensors.

Findings: Neural states exhibited distinct spectral and temporal features relating to symptom severity. Two states exhibited spectral signatures—particularly STN low and high gamma, STN delta/alpha, cortical beta, and cortico-STN beta coherence—that predicted worsening bradykinesia. STN beta oscillations were not consistent predictors of bradykinesia (p = 0.52), but did predict worsening tremor (p < 0.01) and also improvements in dyskinesia severity (p < 0.001), in a state specific manner. These states also displayed compensatory features associated with bradykinesia amelioration, including cortical delta/alpha activity, cortical high gamma, and cortico-STN high gamma coherence. Additionally, we identified a state, marked by STN beta without cortico-STN beta coherence, whose increased lifetimes and occurrence improved motor function (p < 0.001).

Interpretation: Our findings highlight the multidimensional nature of motor impairments in PD and suggest that adaptive interventions targeting state features—rather than single frequency bands—may offer new opportunities for personalised neuromodulation.

Funding: AO: MRC Clinician Scientist Fellowship (MR/W024810/1), Rosetrees Trust/Race Against Dementia Team award, Oxford Hospitals Charity, and the Jon Moulton Charity Trust. TL: China Scholarship Council.

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

saltwater-tensor/PD-neural-dynamics-motor-states

License: AGPL-3.0
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: b4b7158751cd6c6f5dc44bda2d8353066f90589f, 21 April 2026
Languages: Python (13)
Size: 14 files, 13 scripts
Software Heritage: not archived
Found in: “Data sharing statement”
Holds: license file
Not found: README, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: pandas (11 files), NumPy (10 files), Matplotlib (7 files), seaborn (6 files), statsmodels (4 files), SciPy (2 files), TensorFlow (2 files), scikit-posthocs (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
14 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;
  • 13 scripts, each with its path and the digest of its content;
  • 9 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

Due to the sensitive nature of the data, patient-level datasets cannot be made publicly available. Fully anonymised data generated in this study will be made available to qualified researchers upon reasonable request, subject to a data sharing agreement, via the MRC CoRE in Restorative Neural Dynamics data platform (https://data.mrc.ox.ac.uk/).

Code for implementation of this manuscript is available at: https://github.com/saltwater-tensor/PD-neural-dynamics-motor-states.

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

  • Funding: added Rosetrees Trust; China Scholarship Council; Jon Moulton Charity Trust; Medical Research Council

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 9 authors, 3 keywords, 12 MeSH terms, 64 references.

Cite

This paper

Sharma, A., Liu, T., Abdi-Sargezeh, B., Hahn, A., Shcherbakova, M., Neumann, W.-J., Little, S., Starr, P., & Oswal, A. (2026). Dynamic neural states underpin motor symptom severity in Parkinson's disease: a longitudinal analysis of chronic cortico-subthalamic nucleus recordings. EBioMedicine, 128, 106293. https://doi.org/10.1016/j.ebiom.2026.106293

BibTeX

@article{sharma2026dynamic,
author = {Sharma, Abhinav and Liu, Tao and Abdi-Sargezeh, Bahman and Hahn, Amelia and Shcherbakova, Maria and Neumann, Wolf-Julian and Little, Simon and Starr, Philip and Oswal, Ashwini},
title = {{Dynamic neural states underpin motor symptom severity in Parkinson's disease: a longitudinal analysis of chronic cortico-subthalamic nucleus recordings}},
journal = {EBioMedicine},
year = {2026},
month = may,
volume = {128},
pages = {106293},
publisher = {Elsevier},
issn = {2352-3964},
doi = {10.1016/j.ebiom.2026.106293},
url = {https://doi.org/10.1016/j.ebiom.2026.106293},
pmid = {42119302},
pmcid = {PMC13191631}
}

RIS

TY - JOUR
AU - Sharma, Abhinav
AU - Liu, Tao
AU - Abdi-Sargezeh, Bahman
AU - Hahn, Amelia
AU - Shcherbakova, Maria
AU - Neumann, Wolf-Julian
AU - Little, Simon
AU - Starr, Philip
AU - Oswal, Ashwini
TI - Dynamic neural states underpin motor symptom severity in Parkinson's disease: a longitudinal analysis of chronic cortico-subthalamic nucleus recordings
T2 - EBioMedicine
J2 - EBioMedicine
PY - 2026
DA - 2026/05/12
VL - 128
SP - 106293
SN - 2352-3964
PB - Elsevier
DO - 10.1016/j.ebiom.2026.106293
UR - https://doi.org/10.1016/j.ebiom.2026.106293
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.ebiom.2026.106293",
"type": "article-journal",
"title": "Dynamic neural states underpin motor symptom severity in Parkinson's disease: a longitudinal analysis of chronic cortico-subthalamic nucleus recordings",
"container-title": "EBioMedicine",
"author": [
{
"family": "Sharma",
"given": "Abhinav"
},
{
"family": "Liu",
"given": "Tao"
},
{
"family": "Abdi-Sargezeh",
"given": "Bahman"
},
{
"family": "Hahn",
"given": "Amelia"
},
{
"family": "Shcherbakova",
"given": "Maria"
},
{
"family": "Neumann",
"given": "Wolf-Julian"
},
{
"family": "Little",
"given": "Simon"
},
{
"family": "Starr",
"given": "Philip"
},
{
"family": "Oswal",
"given": "Ashwini"
}
],
"container-title-short": "EBioMedicine",
"volume": "128",
"page": "106293",
"DOI": "10.1016/j.ebiom.2026.106293",
"PMID": "42119302",
"PMCID": "PMC13191631",
"ISSN": "2352-3964",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.ebiom.2026.106293",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
12
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41531-026-01372-1 [code]
Varying patterns of association between cortical large-scale networks and subthalamic nucleus activity in Parkinson's disease.
Journal: NPJ Parkinson's disease
In common: seaborn, pandas, SciPy, 2 other tools, Parkinson's, 11 references
[2] doi:10.1038/s41591-026-04434-2 [code]
Adaptive deep brain stimulation for dynamic gait control in Parkinson's disease: a randomized feasibility trial.
Journal: Nature medicine
In common: pandas, SciPy, Matplotlib, 1 other tool, Parkinson's, 10 references
[3] doi:10.1016/j.xcrm.2026.103001
Cortical-to-pallidal beta cascade underlies network pathophysiology in Parkinson's disease.
Journal: Cell reports. Medicine
In common: Parkinson's, 10 references
[4] doi:10.1093/brain/awaf466 [code]
Cortico-basal oscillations index naturalistic movements during deep brain stimulation.
Journal: Brain : a journal of neurology
In common: Parkinson's, 10 references
[5] doi:10.1093/braincomms/fcag245 [code]
Reduced pre-movement subthalamic beta desynchronization marks motor deficit in Parkinson's disease.
Journal: Brain communications
In common: Parkinson's, 10 references
[6] doi:10.1038/s41591-026-04432-4 [code]
Activity-dependent adaptive deep brain stimulation improves gait in Parkinson's disease.
Journal: Nature medicine
In common: SciPy, Matplotlib, NumPy, Parkinson's, 7 references
[7] doi:10.64898/2026.08.12.26350419 [code]
Volitional deep brain stimulation following brain-computer interface training for Parkinson’s disease
Journal: medRxiv (preprint)
In common: statsmodels, seaborn, SciPy, 2 other tools, Parkinson's, 5 references
[8] doi:10.1038/s41531-026-01380-1 [code]
Identifying maximal beta power from directional subthalamic local field potentials in Parkinson's disease.
Journal: NPJ Parkinson's disease
In common: statsmodels, seaborn, pandas, 3 other tools, Parkinson's, 4 references
[9] doi:10.1093/braincomms/fcag329
A meta-analysis of periodic and aperiodic electrophysiological features in Parkinson's disease.
Journal: Brain communications
In common: Parkinson's, 7 references
[10] doi:10.1162/imag.a.1226 [code]
A neuroscientist's guide to neural burst detection.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: statsmodels, seaborn, pandas, 3 other tools, 4 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.