OSCR

Divergent periodic and aperiodic EEG signatures of Propofol versus sevoflurane anesthesia: a comparative neurophysiological study.

Code ↔ Paper

1 match 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 1 match
  1. [1] § Methods › Multivariate statistical framework and interpretability analysis ↔ LocalResources/ConsciousnessClassifier.py, lines 323–429 · score 0.54 · cross validation, fold, probabilities, score, predicted, model

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 · 824 lines · 32 KB · no license · 1 match

  1. """
  2. OOP code for consciousness classification.
  3. JHA
  4. sklearn needs to be version 0.21.2
  5. """
  6. import os
  7. import warnings
  8. import pickle
  9. from time import time
  10. from itertools import combinations
  11. import numpy as np
  12. from sklearn.metrics import roc_curve, auc, accuracy_score
  13. from sklearn import decomposition, discriminant_analysis, svm, linear_model
  14. import matplotlib.pyplot as plt
  15. import pandas as pd
  16. from hmmlearn import hmm as hmm_model
  17. # ignore FutureWarning errors which clutter the outputs
  18. warnings.simplefilter(action='ignore', category=FutureWarning)
  19. ##########################
  20. ###### GLOBAL VARS #######
  21. ##########################
  22. # CNN features are the only ones not being generated online so we need file paths to where its stored on disk
  23. FP_BTLNCKS_VOLUNTEER = 'Data/Volunteer_CNN/btlnck_df.csv'
  24. n_pcs_cnn = 10 #number of principal components for PCA of CNN bottlenecks
  25. if not os.path.exists(FP_BTLNCKS_VOLUNTEER):
  26. warnings.warn(f"filepath to CNN bottlenecks for volunteer data doesn't exist\nFP passed: {FP_BTLNCKS_VOLUNTEER}")
  27. class ConsciousnessClassifier(object):
  28. """Consciousness classifier object that can use a variety of algorithms
  29. to calculate the probability of consciousness from a spectogram of Fp1.
  30. Has been tested on Volunteer data from Purdon et al 2013 and on OR data.
  31. """
  32. def __init__(self, name=None):
  33. """
  34. An object for writing, fitting, and assessing models
  35. """
  36. self.name = name
  37. self.datasets = {}
  38. self.models = {} # option for multiple models
  39. def load_dataset(self, path, names, dsetname, subjects='OR'):
  40. """loads dataset as a dictionary and gets added to the self.datasets attribute
  41. NOTE: This object is built to have separate data sets for train and test (internal validation or external validation)
  42. Parameters
  43. ----------
  44. path : str
  45. directory path to folder containing by case preprocessed data
  46. e.g.~/Dropbox (Partners HealthCare)/HumanSignalsData/projects/consciousness_classifier/volunteer/by_case/
  47. names : list(str)
  48. list of case ids to be loaded from path
  49. dsetname : str
  50. key to add the resulting data_dict to the self.datasets atribute
  51. subjects : str, optional
  52. category of subjects in dataset, either 'OR' or 'volunteer' by default 'OR'
  53. Raises
  54. ------
  55. SystemExit
  56. if something other than 'OR; or 'volunteer' is passed as subjects
  57. """
  58. data_dicts = []
  59. if subjects == 'volunteer':
  60. for tdn in names:
  61. dd = load_volunteer_todict(path, tdn)
  62. if dd is not None:
  63. data_dicts.append(dd)
  64. elif subjects == 'OR':
  65. for tdn in names:
  66. dd = load_OR_todict(path, tdn)
  67. if dd is not None:
  68. data_dicts.append(dd)
  69. # assemble info from each data dict into a dataset
  70. caseid = []
  71. sdata = []
  72. times = []
  73. labels = []
  74. egq = []
  75. for dd in data_dicts:
  76. caseid = np.hstack([caseid,np.full(len(dd['t']),dd['name'])])
  77. dd_sdb = dd['Sdb']
  78. # check for quality
  79. if subjects is 'OR':
  80. dd_egq = dd['egq']
  81. elif subjects is 'volunteer':
  82. dd['egq'] = np.ones(len(dd['l']))
  83. dd_egq = dd['egq']
  84. else:
  85. raise SystemExit(f"Error: subjects parameter is incorrect \n subjects is {subjects} but it must be 'volunteer' or 'OR'")
  86. # apply norm
  87. sdata.append(dd_sdb)
  88. times += list(dd['t'])
  89. labels += list(dd['l'])
  90. egq += list(dd_egq)
  91. ddf = pd.DataFrame(data = np.hstack([np.array([times]).T,
  92. np.hstack(sdata).T,
  93. np.array([egq]).T,
  94. np.array([labels]).T]),
  95. columns=np.hstack(['times', dd['f'], 'egq', 'l']))
  96. ddf['caseid'] = caseid
  97. ddf = ddf.astype({"egq":'int64',"caseid":'category'})
  98. # assemble the dict
  99. dataset_dict = {}
  100. dataset_dict['fs'] = dd['f']
  101. dataset_dict['caseid'] = np.array(caseid)
  102. dataset_dict['labels'] = np.array(labels)
  103. dataset_dict['times'] = np.array(times)
  104. dataset_dict['Sdb'] = np.hstack(sdata)
  105. dataset_dict['subjects'] = subjects
  106. dataset_dict['egq'] = np.array(egq).astype(bool)
  107. dataset_dict['df'] = ddf
  108. self.datasets[dsetname] = dataset_dict
  109. def featurize(self, t_df, fs, v_df=None, which_features=['Sdb','bands','PCA','LDA','CNN']):
  110. """Function for generating the features that a classifier actually uses.
  111. This is called when a classifier is fit and is applied to only the training data.
  112. returns training_features, pca, lda, validation_features
  113. TODO
  114. Parameters
  115. ----------
  116. which_features : list (optional)
  117. which features to featurize
  118. default is all so ['Sdb','bands','PCA','LDA','CNN']
  119. Returns
  120. -------
  121. [type]
  122. [description]
  123. """
  124. # filter out signal dropout, unknown labels ONLY FOR FITTING
  125. good_tdf = t_df[(t_df['l'].isin([0,1])) & (t_df['egq']==1)]
  126. # do PCA and LDA
  127. pca_sdb = decomposition.pca.PCA(n_components=3)
  128. pca_sdb.fit(good_tdf[np.array(fs, dtype='str')].values)
  129. pca_10 = decomposition.pca.PCA(n_components=10)
  130. pca_10.fit(good_tdf[np.array(fs, dtype='str')].values)
  131. lda_sdb = discriminant_analysis.LinearDiscriminantAnalysis(n_components=1)
  132. lda_sdb.fit(good_tdf[np.array(fs, dtype='str')].values, good_tdf['l'].values)
  133. if 'CNN' in which_features:
  134. pca_cnn = fit_pca_cnn(np.unique(good_tdf.caseid))
  135. else:
  136. pca_cnn = []
  137. # temp storage of pca_sdb and lda_sbd
  138. self.pca_lda = [pca_10, lda_sdb]
  139. # apply to the training data (regardless of label)
  140. train_feats = self._featurize_df(t_df,which_features,fs,pca_sdb,lda_sdb,pca_cnn)
  141. # check whether there is a validation df to featurize
  142. if isinstance(v_df, pd.DataFrame):
  143. val_feats = self._featurize_df(v_df,which_features,fs,pca_sdb,lda_sdb,pca_cnn)
  144. return train_feats, val_feats
  145. else:
  146. return train_feats
  147. def _featurize_df(self, df, which_features, fs, pca_sdb, lda_sdb, pca_cnn):
  148. """
  149. Helper function to featurize a data frame
  150. which_features is a list that may contain any of ['Sdb','bands','PCA','LDA','CNN']
  151. assumes features have already been fit.
  152. """
  153. feats_dict = {}
  154. if 'Sdb' in which_features:
  155. # Sdb df unchanged
  156. feats_dict['Sdb'] = df
  157. if 'bands' in which_features:
  158. # tb df from bands
  159. bands = pd.DataFrame(data = Sdb_to_bands(df[np.array(fs, dtype='str')].values, fs))
  160. dfbands = pd.concat([df['times'], bands, df['egq'], df['l'], df['caseid']], axis=1)
  161. feats_dict['bands'] = dfbands
  162. if 'PCA' in which_features:
  163. # same for pca
  164. PCA = pd.DataFrame(data = pca_sdb.transform(df[np.array(fs, dtype='str')].values))
  165. dfPCA = pd.concat([df['times'], PCA, df['egq'], df['l'], df['caseid']], axis=1)
  166. feats_dict['PCA'] = dfPCA
  167. if 'LDA' in which_features:
  168. # same for LDA
  169. LDA = pd.DataFrame(data = lda_sdb.transform(df[np.array(fs, dtype='str')].values))
  170. dfLDA = pd.concat([df['times'], LDA, df['egq'], df['l'], df['caseid']], axis=1)
  171. feats_dict['LDA'] = dfLDA
  172. if 'CNN' in which_features:
  173. # helper fcn for CNN
  174. dfCNN = apply_pca_cnn(pca_cnn,np.unique(df.caseid))
  175. feats_dict['CNN'] = dfCNN
  176. return feats_dict
  177. def add_model(self, ctype='lr', ftype='Sdb', ttype='standard',
  178. mname='lr_standard_Sdb'):
  179. """Adds a model comprised of: a featureset, a classifier, and a method for handling the timeseries.
  180. Classifier types: 'lr', 'svr'
  181. Timeseries types: 'standard', 'hmm2', 'hmmfree'
  182. Feature types: 'Sdb', 'bands', 'PCs', 'LD', 'CNN'
  183. Parameters
  184. ----------
  185. ctype : str, optional
  186. classifier type, by default 'lr'
  187. ftype : str, optional
  188. features for the model, by default 'Sdb'
  189. ttype : str, optional
  190. time series treatment, by default 'standard'
  191. mname : str, optional
  192. model name, by default 'lr_standard_Sdb'
  193. """
  194. # the ultimate classification approach
  195. if ctype == 'svr':
  196. model = svm.SVR(kernel='linear', cache_size=8000)
  197. elif ctype == 'lr':
  198. model = linear_model.LogisticRegression()
  199. # the method for treatment of the timeseries
  200. model.timeseries = ttype
  201. if ttype == 'standard':
  202. model.hmm = None
  203. elif ttype == 'hmm2':
  204. hmm = hmm_model.GaussianHMM(2, algorithm='viterbi', n_iter=10)
  205. model.hmm = hmm
  206. elif ttype == 'hmmfree':
  207. hmm = hmm_model.GaussianHMM(6, algorithm='viterbi', n_iter=10)
  208. model.hmm = hmm
  209. else:
  210. raise Exception("The given timeseries treatment does not fall into an approved type (standard, hmm2, hmmfree).")
  211. # the features fed into the timeseries treatment
  212. model.ftype = ftype
  213. # and now append it
  214. self.models[mname] = model
  215. def train_model(self, mname, training_dname, timer=False, which_features=['Sdb','bands','PCA','LDA','CNN']):
  216. """Fits model. The classifier knows which features to take and whether
  217. or not to HMM it.
  218. Parameters
  219. ----------
  220. mname : str
  221. model name
  222. training_dname : str
  223. dataset to fit on
  224. timer : bool, optional
  225. whether we want it trained, by default False
  226. """
  227. if timer is True:
  228. lap = laptimer()
  229. # collect the data
  230. model = self.models[mname]
  231. dataset = self.datasets[training_dname]
  232. ddf = dataset['df']
  233. fs = dataset['fs']
  234. # do the featurization step
  235. train_feats = self.featurize(ddf, fs, which_features=which_features)
  236. # get the specific feature dict
  237. train_feat = train_feats[model.ftype]
  238. # drop cols
  239. nonvalue_cols = ['times', 'egq', 'l', 'caseid']
  240. # perform the timeseries analysis by taking only eeg quality spots
  241. if model.timeseries == 'standard':
  242. # no treatment of the timeseries as a timeseries
  243. training_series = train_feat[train_feat['egq']==1].drop(nonvalue_cols, axis=1).values
  244. training_labels = train_feat[train_feat['egq']==1]['l']
  245. else:
  246. # get the training values from the HMM timeseries
  247. hmm = model.hmm
  248. train_lengths = _continuous_lengths(train_feat)
  249. hmm.fit(train_feat[train_feat['egq']==1].drop(nonvalue_cols, axis=1).values,
  250. train_lengths)
  251. # calculate posterior probabilities for each state in order to train logistic regression
  252. posteriors = hmm.score_samples(
  253. train_feat[train_feat['egq']==1].drop(nonvalue_cols, axis=1).values,
  254. train_lengths)[1]
  255. # ## calcualte AIC for model parameterized in this way
  256. # logprob = hmm.decode(train_feat, algorithm='viterbi')[0]
  257. # n_params = 2*hmm.n_components*hmm.n_features +(hmm.n_components)**2 -1
  258. # aic = 2*(n_params) - 2*logprob
  259. # hmm.aic = aic
  260. training_series = posteriors
  261. training_labels = train_feat[train_feat['egq']==1]['l']
  262. # perform training, then get val py
  263. model.fit(training_series, training_labels)
  264. model.isfit = True
  265. # used to featurize the validation data
  266. model.training_info = [ddf, fs]
  267. # give the time of the fitting
  268. if timer is True:
  269. print(f"Processing time: {np.round(lap(),3)}")
  270. def crossvalidation(self, mname, dname, n_held=1, timer=False,
  271. features=['Sdb', 'bands', 'PCA', 'LDA', 'CNN']):
  272. """Performs cross-validation noting that each time, a different n_held must be held out. We want to do every combination available.
  273. Parameters
  274. ----------
  275. mname : str
  276. model name
  277. dname : str
  278. dataset used for crossval
  279. n_held : int, optional
  280. number held out in crossval, by default 1
  281. timer : bool, optional
  282. whether to print the time taken to run, by default False
  283. features : list, optional
  284. which features should be generated, by default ['Sdb', 'bands', 'PCA', 'LDA', 'CNN']
  285. Returns
  286. -------
  287. list
  288. crossvalidation AUCs
  289. """
  290. # collect the data
  291. model = self.models[mname]
  292. dataset = self.datasets[dname]
  293. ddf = dataset['df']
  294. fs = dataset['fs']
  295. # get combinations for training / val splits
  296. unique_trials = np.unique(dataset['caseid'])
  297. combs = list(combinations(unique_trials, len(unique_trials)-n_held))
  298. # what we collect from each crossval iteration
  299. model_performance = [] # AUC over single left out case
  300. for fold in combs:
  301. # split for featurization
  302. train_df = ddf[ddf['caseid'].isin(fold)].reset_index(drop=True)
  303. val_df = ddf[~ddf['caseid'].isin(fold)].reset_index(drop=True)
  304. # do the featurization step
  305. train_feats, val_feats = self.featurize(train_df, fs, v_df=val_df,
  306. which_features=features)
  307. # get the specific feature dict
  308. train_feat = train_feats[model.ftype]
  309. val_feat = val_feats[model.ftype]
  310. # drop cols
  311. nonvalue_cols = ['times', 'egq', 'l', 'caseid']
  312. # perform the timeseries analysis by taking only eeg quality spots
  313. if model.timeseries == 'standard':
  314. # no treatment of the timeseries as a timeseries
  315. training_series = train_feat[train_feat['egq']==1].drop(nonvalue_cols, axis=1).values
  316. training_labels = train_feat[train_feat['egq']==1]['l']
  317. validation_series = val_feat[val_feat['egq']==1].drop(nonvalue_cols, axis=1).values
  318. validation_labels = val_feat[val_feat['egq']==1]['l']
  319. else:
  320. # get the training values from the HMM timeseries
  321. hmm = model.hmm
  322. train_lengths = _continuous_lengths(train_feat)
  323. hmm.fit(train_feat[train_feat['egq']==1].drop(nonvalue_cols, axis=1).values,
  324. train_lengths)
  325. # calculate posterior probabilities for each state in order to train logistic regression
  326. posteriors = hmm.score_samples(
  327. train_feat[train_feat['egq']==1].drop(nonvalue_cols, axis=1).values,
  328. train_lengths)[1]
  329. # ## calcualte AIC for model parameterized in this way
  330. # logprob = hmm.decode(train_feat, algorithm='viterbi')[0]
  331. # n_params = 2*hmm.n_components*hmm.n_features +(hmm.n_components)**2 -1
  332. # aic = 2*(n_params) - 2*logprob
  333. # hmm.aic = aic
  334. training_series = posteriors
  335. training_labels = train_feat[train_feat['egq']==1]['l']
  336. val_lengths = _continuous_lengths(val_feat)
  337. try:
  338. val_posteriors = hmm.score_samples_fwd(
  339. val_feat[val_feat['egq']==1].drop(nonvalue_cols, axis=1).values,
  340. val_lengths)[1]
  341. except:
  342. print('WARNING: You are not using the modified version of HMM learn')
  343. print('Your classifier may be using the backward algorithm to predict consciousness')
  344. print('This does not affect the performance of the model. It only means this classifier could not be used in real time')
  345. print('For access to the forward-only hmmlearn see https://github.com/benyameister/hmmlearn/blob/master/README.rst')
  346. val_posteriors = hmm.score_samples(
  347. val_feat[val_feat['egq']==1].drop(nonvalue_cols, axis=1).values,
  348. val_lengths)[1]
  349. validation_series = val_posteriors
  350. validation_labels = val_feat[val_feat['egq']==1]['l']
  351. # perform training, then get val py
  352. model.fit(training_series, training_labels)
  353. model.isfit = True
  354. py = model.predict_proba(validation_series)[:,1]
  355. # save roc from each split
  356. fpr, tpr = roc_curve(validation_labels, py)[:2]
  357. auc_split = auc(fpr, tpr)
  358. model_performance.append(auc_split)
  359. return model_performance
  360. def save_model(self, path, mname):
  361. """
  362. Deprecated.
  363. """
  364. model = self.models[mname]
  365. pickle.dump(model, open(path, 'wb'))
  366. def load_model(self, path, mname):
  367. """Deprecated."""
  368. self.models[mname] = pickle.load(open(path, 'rb'))
  369. def validate_model(self, mname, validation_dname, timer=False):
  370. """applies a model to data within dsetname dataset.
  371. the resulting predictions are appended to the dataset with mname.
  372. ensures that norming is same.
  373. Note that this predicts *everything*, and nan labels must be stripped later (similarly hmm does predict nan labels but not egq=0 because data must be continuous).
  374. Also note that the features are generated using the PCA or LDA fit from the training data, which is now part of the model.
  375. """
  376. if timer is True:
  377. lap = laptimer()
  378. # collect the data
  379. model = self.models[mname]
  380. dataset = self.datasets[validation_dname]
  381. ddf = dataset['df']
  382. fs = dataset['fs']
  383. ftype=model.ftype
  384. # ensure model has been fit
  385. assert model.isfit, "Model is not yet fit!"
  386. # do the featurization step
  387. tddf, tfs = model.training_info
  388. assert all(tfs==fs), "Training/validation frequencies of MTSGs must be same!"
  389. val_feats = self.featurize(tddf, tfs, v_df=ddf, which_features=[ftype])[1]
  390. # get the specific feature dict
  391. val_feat = val_feats[model.ftype]
  392. # drop cols
  393. nonvalue_cols = ['times', 'egq', 'l', 'caseid']
  394. # perform the timeseries analysis by taking only eeg quality spots
  395. if model.timeseries == 'standard':
  396. # no treatment of the timeseries as a timeseries
  397. validation_series = val_feat[val_feat['egq']==1].drop(nonvalue_cols, axis=1).values
  398. else:
  399. # get the training values from the HMM timeseries
  400. hmm = model.hmm
  401. val_lengths = _continuous_lengths(val_feat[val_feat['egq']==1])
  402. try:
  403. val_posteriors = hmm.score_samples_fwd(
  404. val_feat[val_feat['egq']==1].drop(nonvalue_cols, axis=1).values,
  405. val_lengths)[1]
  406. except:
  407. print('WARNING: You are not using the modified version of HMM learn')
  408. print('Your classifier may be using the backward algorithm to predict consciousness')
  409. print('This does not affect the performance of the model. It only means this classifier could not be used in real time')
  410. print('For access to the forward-only hmmlearn see https://github.com/benyameister/hmmlearn/blob/master/README.rst')
  411. val_posteriors = hmm.score_samples(
  412. val_feat[val_feat['egq']==1].drop(nonvalue_cols, axis=1).values,
  413. val_lengths)[1]
  414. validation_series = val_posteriors
  415. py = model.predict_proba(validation_series)[:,1]
  416. # only return wehre we made predictions
  417. ddf_val = ddf[ddf['egq']==1].copy()
  418. ddf_val['py'] = py
  419. # append the validation result to the model itself
  420. if not hasattr(model, "val_result"):
  421. model.val_result = {}
  422. model.val_result[validation_dname] = ddf_val
  423. model.isval = True
  424. self.models[mname] = model
  425. # give the time of the fitting
  426. if timer is True:
  427. print(f"Processing time: {np.round(lap(),3)}")
  428. def roc_auc(self, mname, dname, cases='all'):
  429. """returns fpr, tpr, auc
  430. strips regions with bad eeg quality or NANs
  431. """
  432. ddf = self.models[mname].val_result[dname]
  433. if cases=='all':
  434. validation_l = ddf[ddf['egq']==1]['l'].values
  435. validation_py = ddf[ddf['egq']==1]['py'].values
  436. vl = validation_l[~np.isnan(validation_l)]
  437. py = validation_py[~np.isnan(validation_l)]
  438. elif type(cases) is list:
  439. validation_l = ddf[(ddf['egq']==1) & (ddf['caseid'].isin(cases))]['l'].values
  440. validation_py = ddf[(ddf['egq']==1) & (ddf['caseid'].isin(cases))]['py'].values
  441. vl = validation_l[~np.isnan(validation_l)]
  442. py = validation_py[~np.isnan(validation_l)]
  443. fpr, tpr, thr = roc_curve(vl, py)[:3]
  444. auct = auc(fpr, tpr)
  445. sens_plus_spec = (1-fpr)+tpr
  446. thr_opt = thr[np.argmax(sens_plus_spec)]
  447. return fpr, tpr, thr, auct, thr_opt
  448. def acc(self, mname, dname, thr=0.5, cases='all'):
  449. """returns fpr, tpr, auc
  450. strips regions with bad eeg quality or NANs
  451. """
  452. ddf = self.models[mname].val_result[dname]
  453. if cases=='all':
  454. validation_l = ddf[ddf['egq']==1]['l'].values
  455. validation_py = ddf[ddf['egq']==1]['py'].values
  456. vl = validation_l[~np.isnan(validation_l)]
  457. py = validation_py[~np.isnan(validation_l)]
  458. elif type(cases) is list:
  459. validation_l = ddf[(ddf['egq']==1) & (ddf['caseid'].isin(cases))]['l'].values
  460. validation_py = ddf[(ddf['egq']==1) & (ddf['caseid'].isin(cases))]['py'].values
  461. vl = validation_l[~np.isnan(validation_l)]
  462. py = validation_py[~np.isnan(validation_l)]
  463. ys = py>=thr
  464. acc = accuracy_score(ys, vl)
  465. return acc
  466. def plot_roc(self, mname, dsetname, ax=None, label='', c='b', ls='-'):
  467. """ plots the roc """
  468. if ax is None:
  469. ax = plt.subplot()
  470. fpr, tpr, thr, auct, _ = self.roc_auc(mname, dsetname)
  471. ax.plot(fpr, tpr, label=f'{label} AUC = {np.round(auct, 3)}', c=c,
  472. ls=ls)
  473. ax.plot([0, 1], [0, 1], 'k:')
  474. ax.set_xlabel('FPR')
  475. ax.set_ylabel('TPR')
  476. ax.legend()
  477. def save_inference_table(self, path, mname, dname):
  478. """ Deprecated. """
  479. inf_tab = self.models[mname].val_result[dname]
  480. inf_tab.to_csv(path+mname+'.csv')
  481. # utility functions
  482. class laptimer:
  483. """
  484. Whenever you call it, it times laps.
  485. """
  486. def __init__(self):
  487. self.time = time()
  488. def __call__(self):
  489. ret = time() - self.time
  490. self.time = time()
  491. return ret
  492. def __str__(self):
  493. return "%.3E" % self()
  494. def __repr__(self):
  495. return "%.3E" % self()
  496. def save_nparray(filename, nparray, colnames=None):
  497. """
  498. Uses pandas to save a numpy array with column headers.
  499. """
  500. assert(len(colnames)) == nparray.shape[1], "columns do not match table"
  501. output_df = pd.DataFrame(data=nparray, columns=colnames)
  502. output_df.to_csv(filename, index=False)
  503. def save_inference_table(filename, table):
  504. """helper function for saving inference tables"""
  505. save_nparray(filename, table, colnames=['case_id', 't', 'p_y', 'y'])
  506. def load_volunteer_todict(path, name):
  507. """
  508. Loads .csv files into a dict. I find this a useful utility.
  509. """
  510. f = np.genfromtxt(path + '/' + name + '_f.csv', delimiter=',')
  511. t = np.genfromtxt(path + '/' + name + '_t.csv', delimiter=',')
  512. l = np.genfromtxt(path + '/' + name + '_l.csv', delimiter=',')
  513. Sdb = np.genfromtxt(path + '/' + name + '_Sdb.csv', delimiter=',')
  514. egq = np.ones(len(t))
  515. return {'f': f, 't': t, 'Sdb': Sdb, 'l': l, 'name': name,
  516. 'egq': egq}
  517. def load_OR_todict(path, name):
  518. """
  519. Loads .csv files into a dict including events information from CL files
  520. """
  521. try:
  522. f = np.genfromtxt(path + '/' + name + '_f.csv', delimiter=',')
  523. t = np.genfromtxt(path + '/' + name + '_t.csv', delimiter=',')
  524. l = np.genfromtxt(path + '/' + name + '_l.csv', delimiter=',')
  525. Sdb = np.genfromtxt(path + '/' + name + '_Sdb.csv', delimiter=',')
  526. # events = np.genfromtxt(path +'/' + name + '_events.csv',
  527. # delimiter=',', usecols=[0, 1], dtype="str")
  528. egq = np.genfromtxt(path + '/' + name + '_EEGquality.csv',
  529. delimiter=',')
  530. return_dict = {'f': f, 't': t, 'Sdb': Sdb, 'l':l, #'events': events,
  531. 'name': name, 'egq':egq}
  532. # get bolus if it's real
  533. if os.path.exists(path + '/' + name + '_bolus.csv'):
  534. bolus = np.genfromtxt(path + '/' + name + '_bolus.csv',
  535. delimiter=',', dtype='str')
  536. return_dict['bolus'] = bolus
  537. # and infusion if it's real
  538. if os.path.exists(path + '/' + name + '_infusion.csv'):
  539. infusion = np.genfromtxt(path + '/' + name + '_infusion.csv',
  540. delimiter=',', dtype='str')
  541. return_dict['infusion'] = infusion
  542. # and gas if it's real
  543. if os.path.exists(path + '/' + name + '_gas.csv'):
  544. gas = np.genfromtxt(path + '/' + name + '_gas.csv',
  545. delimiter=',', dtype='str')
  546. return_dict['gas'] = gas
  547. return return_dict
  548. except OSError:
  549. print(f"Incomplete data for {name}")
  550. except ValueError:
  551. print(f"Incorrect CSV format for {name}")
  552. def _continuous_lengths(data_df, dt=2):
  553. """
  554. calcualtes lengths of continuous observations of eeg data for HMM processing
  555. This method is written for external validation data which has spontaneous
  556. drop out of signal. That is why a 'egq' array is expected from the data _dict
  557. This method still works for volunteer data, though it is analogous to stacking
  558. the lengths of each case
  559. Parameters
  560. ----------
  561. data_df : pandas dataframe
  562. should have entries 'egq' and 'caseid'
  563. dt: float
  564. spacing in t
  565. Returns
  566. -------
  567. lengths : numpy array
  568. array of lengths. Each length is an integer number that is the number of continuous samples.
  569. Easy check that it is being computed correctly is sum(lengths) = len(goodeeg_times)
  570. """
  571. lengths = []
  572. goodeeg_times = data_df[data_df['egq']==1]['times'].values
  573. length = 1
  574. for ti, t in enumerate(goodeeg_times[:-1]):
  575. if goodeeg_times[ti+1]-t==2:
  576. length+=1
  577. else:
  578. lengths.append(length)
  579. length=1
  580. lengths.append(length)
  581. return np.array(lengths)
  582. def hmm_aic(n_states_options,training_data,training_lengths,timer=True,plot=True):
  583. """
  584. calcualte AIC for a number of different states based on training data
  585. to see if there is an ideal number of states for HMMfree
  586. TODO - params and results
  587. Parameters
  588. ----------
  589. Results
  590. -------
  591. """
  592. AIC = []
  593. for k in n_states_options:
  594. if timer is True:
  595. lap = laptimer()
  596. model = hmm_model.GaussianHMM(
  597. k,
  598. algorithm='viterbi',
  599. n_iter=10)
  600. model.fit(training_data.transpose(), training_lengths)
  601. logprob = model.decode(training_data.transpose(),algorithm='viterbi')[0]
  602. n_params = 2*model.n_components*model.n_features +(model.n_components)**2 -1
  603. aic = aic = 2*(n_params) - 2*logprob
  604. AIC.append(aic)
  605. if timer is True and k>15:
  606. print(f"finished generating model with {k} states")
  607. print(f"Processing time: {np.round(lap(), 1)} seconds")
  608. plt.figure(figsize=(4,1.5))
  609. plt.plot(n_states_options,AIC)
  610. plt.title("AIC")
  611. return AIC
  612. def Sdb_to_bands(Sdb, fi):
  613. """Gets total power in slow-delta, theta, alpha, beta, gamma(?) range.
  614. """
  615. S = 10**(Sdb.T/10)
  616. sr = np.logical_and(fi>=0, fi<=1.)
  617. dr = np.logical_and(fi>1, fi<4)
  618. tr = np.logical_and(fi>=4, fi<8)
  619. ar = np.logical_and(fi>=8, fi<13)
  620. br = np.logical_and(fi>=13, fi<25)
  621. gr = np.logical_and(fi>=25, fi<50)
  622. slow_db = 10*np.log10(S[sr,:].sum(0)+1E-10)
  623. delta_db = 10*np.log10(S[dr,:].sum(0)+1E-10)
  624. theta_db = 10*np.log10(S[tr,:].sum(0)+1E-10)
  625. alpha_db = 10*np.log10(S[ar,:].sum(0)+1E-10)
  626. beta_db = 10*np.log10(S[br,:].sum(0)+1E-10)
  627. gamma_db = 10*np.log10(S[gr,:].sum(0)+1E-10)
  628. return np.vstack([slow_db, delta_db, theta_db, alpha_db, beta_db, gamma_db]).T
  629. def fit_pca_cnn(case_ids_train):
  630. """transform CNN botlenecks into its PC's
  631. NOTE: only uses volunteer bottlenecks right now because OR ones havent been generated
  632. Parameters
  633. ----------
  634. case_id_train : np.ndarray
  635. list of integers corresponding to unique case ids
  636. Returns
  637. -------
  638. cnn_pcs : pd.DataFrame
  639. first 3 principal component of CNN bottleneck values for each window
  640. also contains times and labels because they don't line up with other features
  641. pca_cnn : decomposition.pca.PCA
  642. fitted PCA to be used for validation data
  643. """
  644. btlncks = pd.read_csv(FP_BTLNCKS_VOLUNTEER)
  645. # filter btlncks to only use cases in training set
  646. train_case_inds = [case_id in case_ids_train for case_id in btlncks.case_id]
  647. btlncks = btlncks.loc[train_case_inds,:]
  648. pca_cnn = decomposition.pca.PCA(n_components=n_pcs_cnn)
  649. pca_cnn.fit(btlncks.loc[:,'btlnck_0':])
  650. return pca_cnn
  651. def apply_pca_cnn(pca_cnn, case_ids):
  652. """calculate principal components for validation data using prefitted pca
  653. NOTE: only uses volunteer bottlenecks right now because OR ones havent been generated
  654. Parameters
  655. ----------
  656. pca_cnn : decomposition.pca.PCA
  657. fitted PCA to be used for validation data
  658. case_id_train : np.ndarray
  659. list of strings corresponding to unique case ids
  660. Returns
  661. -------
  662. cnn_pcs : pd.DataFrame
  663. first 3 principal component of CNN bottleneck values for each window
  664. also contains times and labels because they don't line up with other features
  665. """
  666. btlncks = pd.read_csv(FP_BTLNCKS_VOLUNTEER)
  667. # filter btlncks to only use cases in training set
  668. train_case_inds = [case_id in case_ids for case_id in btlncks.case_id]
  669. btlncks = btlncks.loc[train_case_inds,:]
  670. pcs = pca_cnn.transform(btlncks.loc[:,'btlnck_0':])
  671. # output as dataframe so it keeps associated t and labels because has different alignment than other features
  672. column_names = [f'PC{n+1}' for n in range(n_pcs_cnn)]
  673. cnn_pcs = pd.DataFrame(pcs, columns=column_names)
  674. cnn_pcs['times'] = btlncks.t.values
  675. cnn_pcs['l'] = btlncks.is_conscious.values
  676. cnn_pcs['egq'] = np.array([1]*len(cnn_pcs['l']))
  677. cnn_pcs['caseid'] = btlncks.case_id
  678. return cnn_pcs

ConsciousnessClassifier.py at commit fe3bbdc, no license · at the source

Overview

Authors: Ping Shen1, Miao Da1, Zhongxia Shen1
  1. Huzhou Third Municipal Hospital, The Affiliated Hospital of Wenzhou Medical University, Huzhou, China
Institutions: Wenzhou Medical University (China)
Journal: Frontiers in medicine, volume 13, article 1794626
Dates: received 23 January 2026; accepted 23 March 2026; published online 28 April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3389/fmed.2026.1794626 · PMID 42131603 · PMCID PMC13161752 · OpenAlex W7156607278
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), clinical / translational (subfield)
Methods: Spectral & time-frequency, Statistics, Machine learning, Preprocessing, Complexity
Keywords: anesthesia, aperiodic, components, electroencephalography, general, Propofol, sevoflurane
Topic: Anesthesia and Sedative Agents (Anesthesiology and Pain Medicine, Medicine), according to OpenAlex
Citations: not cited yet (Europe PMC); 48 references in the paper

Abstract

Background: Current clinical anesthesia monitors often utilize drug-invariant indices that simplify cortical dynamics, potentially overlooking pharmacological nuances. While Propofol and Sevoflurane are both GABAergic, they may induce distinct neural states. This study aimed to identify the divergent periodic and aperiodic EEG signatures that distinguish these two regimens during the steady-state maintenance phase.

Methods: A retrospective analysis was conducted using data from an open-access clinical database comprising 44 surgical patients (Propofol group, n = 27; Sevoflurane group, n = 17). EEG data were extracted during the pharmacological steady-state (20 min post-loss of consciousness to 10 min pre-end of surgery). Seventeen features, including relative band power, alpha peak frequency, and aperiodic components, were then derived. A multivariate statistical framework utilizing subject-independent cross-validation and SHapley Additive exPlanations (SHAP) analysis was implemented to identify and rank the most discriminatory biological markers.

Results: The multivariate model achieved high discriminatory performance with a rigorous subject-level accuracy of 91.43%. Relative theta power, theta-to-alpha ratio, and alpha peak frequency were identified as the primary differentiators, occupying the top tiers of the SHAP importance ranking. Specifically, the Sevoflurane group exhibited a distinct elevation in theta-band prominence and a significant downward shift in alpha peak frequency (8.78 Hz vs. 10.88 Hz for Propofol). Furthermore, the aperiodic exponent emerged as a critical discriminatory feature, demonstrating a significantly steeper background spectral slope under Sevoflurane (2.37 vs. 2.07 for Propofol, p = 0.039). Conversely, alpha bandwidth (p = 0.263) and signal complexity measures (e.g., spectral entropy, p = 0.721) provided negligible discriminatory value.

Conclusion: Propofol and Sevoflurane maintain unconsciousness via distinct neurophysiological regimes. The differentiation between these two agents is primarily driven by structural oscillatory shifts, specifically theta-band prominence and alpha peak deceleration, along with steepened aperiodic background dynamics, rather than periodic bandwidth or overall signal complexity. These findings underscore the distinct cortical modulation patterns of different GABAergic anesthetics and support the development of agent-specific, multidimensional monitoring protocols to enhance precision in individualized brain state assessment.

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 1 match between paragraphs and lines of code.

johnabel/GABAergic_unconsciousness

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: fe3bbdc5dc63c2aad076e7a1bc099c8b6baa866b, 20 February 2021
Languages: Python (12)
Size: 22 files, 12 scripts
Software Heritage: not archived
Found in: “Data availability statement”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (12 files), NumPy (12 files), pandas (8 files), SciPy (8 files), scikit-learn (7 files), seaborn (6 files)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
13 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;
  • 12 scripts, each with its path and the digest of its content;
  • 1 match 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 availability statement

Publicly available datasets were analyzed in this study. This data can be found at: “GABAergic Anesthetic-Induced Unconsciousness and Recovery” https://github.com/johnabel/GABAergic_unconsciousness.

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, 30 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 3 authors, 7 keywords, 48 references.

Cite

This paper

Shen, P., Da, M., & Shen, Z. (2026). Divergent periodic and aperiodic EEG signatures of Propofol versus sevoflurane anesthesia: a comparative neurophysiological study. Frontiers in medicine, 13, 1794626. https://doi.org/10.3389/fmed.2026.1794626

BibTeX

@article{shen2026divergent,
author = {Shen, Ping and Da, Miao and Shen, Zhongxia},
title = {{Divergent periodic and aperiodic EEG signatures of Propofol versus sevoflurane anesthesia: a comparative neurophysiological study}},
journal = {Frontiers in medicine},
year = {2026},
month = apr,
volume = {13},
pages = {1794626},
publisher = {Frontiers Media SA},
issn = {2296-858X},
doi = {10.3389/fmed.2026.1794626},
url = {https://doi.org/10.3389/fmed.2026.1794626},
pmid = {42131603},
pmcid = {PMC13161752}
}

RIS

TY - JOUR
AU - Shen, Ping
AU - Da, Miao
AU - Shen, Zhongxia
TI - Divergent periodic and aperiodic EEG signatures of Propofol versus sevoflurane anesthesia: a comparative neurophysiological study
T2 - Frontiers in medicine
J2 - Front Med (Lausanne)
PY - 2026
DA - 2026/04/28
VL - 13
SP - 1794626
SN - 2296-858X
PB - Frontiers Media SA
DO - 10.3389/fmed.2026.1794626
UR - https://doi.org/10.3389/fmed.2026.1794626
LA - en
ER -

CSL-JSON

{
"id": "10.3389/fmed.2026.1794626",
"type": "article-journal",
"title": "Divergent periodic and aperiodic EEG signatures of Propofol versus sevoflurane anesthesia: a comparative neurophysiological study",
"container-title": "Frontiers in medicine",
"author": [
{
"family": "Shen",
"given": "Ping"
},
{
"family": "Da",
"given": "Miao"
},
{
"family": "Shen",
"given": "Zhongxia"
}
],
"container-title-short": "Front Med (Lausanne)",
"volume": "13",
"page": "1794626",
"DOI": "10.3389/fmed.2026.1794626",
"PMID": "42131603",
"PMCID": "PMC13161752",
"ISSN": "2296-858X",
"publisher": "Frontiers Media SA",
"URL": "https://doi.org/10.3389/fmed.2026.1794626",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
28
]
]
}
}

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.1002/mco2.70980 [code]
An Intraoperative EEG Biomarker for Postoperative Delirium Predicting Based on Interpretable Deep Learning Framework.
Journal: MedComm
In common: seaborn, scikit-learn, pandas, 3 other tools, EEG, clinical / translational, 2 references
[2] doi:10.1093/cercor/bhag113 [code]
Long-term reliability and stability of parameterized resting state EEG: evidence from a five-year follow-up.
Journal: Cerebral cortex (New York, N.Y. : 1991)
In common: seaborn, scikit-learn, pandas, 3 other tools, EEG, 2 references
[3] doi:10.1038/s42003-026-09769-7
Distinct origins of human low and high alpha rhythms revealed by simultaneous EEG-SEEG.
Journal: Communications biology
In common: EEG, 4 references
[4] doi:10.7554/elife.100605 [code]
Age-related changes in ‘cortical’ 1/f dynamics are linked to cardiac activity
Journal: n/a
In common: seaborn, scikit-learn, pandas, 3 other tools, 2 references
[5] doi:10.1111/ejn.70255 [code]
A Systematic Review of Aperiodic Neural Activity in Clinical Investigations
Journal: n/a
In common: seaborn, pandas, Matplotlib, 1 other tool, EEG, clinical / translational, 2 references
[6] doi:10.1038/s42003-026-10205-z [code]
Source-space EEG alpha activity reveals brain age gaps due to neurodegeneration and disparity.
Journal: Communications biology
In common: seaborn, scikit-learn, pandas, 3 other tools, EEG, 1 reference
[7] doi:10.1126/sciadv.adz6517 [code]
Corticosterone-linked microglial activity underpins sexually dimorphic neuroplasticity after ketamine anesthesia.
Journal: Science advances
In common: seaborn, scikit-learn, pandas, 3 other tools, 1 reference
[8] doi:10.1038/s41467-026-72454-0 [code]
Dynamic neuronal ensembles encode burst-suppression revealed by cortex-wide optical-electrical interfaces.
Journal: Nature communications
In common: seaborn, scikit-learn, pandas, 3 other tools, 1 reference
[9] doi:10.1038/s41593-026-02205-3 [code]
Competitive interactions shape mammalian brain network dynamics and computation.
Journal: Nature neuroscience
In common: seaborn, scikit-learn, pandas, 3 other tools, 1 reference
[10] doi:10.1038/s41467-026-74227-1 [code]
Age-related changes in behavioural and neural variability in a decision-making task.
Journal: Nature communications
In common: seaborn, scikit-learn, pandas, 3 other tools, 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.