def andreas_power_features(eeg_signal, fs=128): for i in (np.arange(eeg_signal.shape[0] / 100) + 1): if i == 1: df = yasa.bandpower(eeg_signal[0:int(100 * i), :], sf=fs) else: df = df.append( yasa.bandpower(eeg_signal[int(100 * (i - 1)):int(100 * i), :], sf=fs)) df = df.set_index(np.arange(eeg_signal.shape[0])) df = df.drop(columns=["FreqRes", "Relative"], axis=1) return np.array(df)
def __calculateRelativeFrequencyBands(self, epochSeries : pd.Series) -> pd.Series: ''' Calculates the frequecny bands from the given time series (epochSeries) and returns a series of dataframes with the frequency bands The Dataframe looks then like this: (depding on the given frequency bands) Channel | Delta | Theta | Alpha | ... -------------------------------------- channel_1 | 0.53 | 0.323 | ... channel_2 | 0.53 | 0.323 | ... .... | .... | Info for the bandpower function: https://raphaelvallat.com/yasa/build/html/generated/yasa.bandpower.html ''' epochFeatureSeries = [] for epoch_df in epochSeries: dataNpArray = self.__convertDataFrameToNumpyArray(epoch_df) # calculate the bandpower for this epoch bandpower_df = yasa.bandpower(data=dataNpArray, sf=self.samplingRate, ch_names=epoch_df.columns, win_sec=4, # e.g. for a lower frequency of interest of 0.5 Hz, the window length should be at least 2 * 1 / 0.5 = 4 seconds relative=True, # then the bandpower is already between 0 and 1 bands=self.frequencyBands, kwargs_welch=self.kwargsWelch) # e.g. (0.5, 4, ‘Delta’) epochFeatureSeries.append(bandpower_df) return epochFeatureSeries
def run(self, data: List[DataFrame], **kwargs) -> List[DataFrame]: data = [d.swapaxes('index', 'columns') for d in data] data = [ bandpower(d.to_numpy(), kwargs['lowest_frequency'], d.index) for d in data ] data = [d.loc[:, self.bands] for d in data] return data
def obtain_spectral_features(data, win=4.0, sf=250): win_sec = win data = data.squeeze() * 1e6 #Go back to volts bandpower = yasa.bandpower(data, sf=sf, ch_names=EEG_channels, win_sec=win_sec, bands=[(0.5, 4, 'Delta'), (4, 8, 'Theta'), (8, 12, 'Alpha'), (12, 30, 'Beta'), (30, 50, 'Gamma')]) return bandpower[bands]
def compute_frequency_features(signal, fs, ch_names=['EEG', 'EEG(sec)'], normalize_signal=True): if normalize_signal: sscaler = StandardScaler() num_ch = signal.shape[0] signal = np.reshape(sscaler.fit_transform(np.reshape(signal, (-1, 1))), (num_ch, -1)) return bandpower(signal, sf=fs, ch_names=ch_names, relative=True)[ # 'Gamma' removed ['Delta', 'Theta', 'Alpha', 'Beta']].mean().values
def bandpower(df: pd.DataFrame) -> pd.DataFrame: df["bandpower"] = [[] for _ in range(len(df))] for i, row in df.iterrows(): timestamps, *ch = zip(*row["raw_data"]) data = np.array(ch).T rawarray = raw_to_mne(data) df_power = yasa.bandpower(rawarray) df_power = df_power.reset_index()[ ["Chan", "Delta", "Theta", "Alpha", "Sigma", "Beta", "Gamma"] ] df.at[i, "bandpower"] = df_power.values.tolist() return df
def make_prediction(self, chunk): ################### ## Get new samples# ################### chunk = np.array(chunk) #Should be of size (125,30) # Roll old data to make space for the new chunk self.dataBuffer = np.roll(self.dataBuffer, -self.chunk_size, axis=0) # Buffer Shape (Time*SF, channels) # Add chunk in the last 125 rows of the data buffer. 250 samples ==> 1 second. self.dataBuffer[-self.chunk_size:, :] = chunk[:, :] # Add chunk in the last 125 rows. 250 samples ==> 1 second. ###################### ##Calculate Features## ###################### # Check that data is in the correct range # check_units = [0.2 < abs(self.dataBuffer[-2*self.chunk_size:, 2].min()) < 800, # 1 < abs(self.dataBuffer[-2*self.chunk_size:, 7].max()) < 800, # 0.2 < abs(self.dataBuffer[-2*self.chunk_size:, 15].min()) < 800] # assert all(check_units), \ # "Check the units of the data that is about to be process. " \ # "Data should be given as uv to the get bandpower coefficients function" \ # + str(check_units) # Get bandPower coefficients win_sec = 0.95 bandpower = yasa.bandpower(self.dataBuffer[-2*self.chunk_size:, :].transpose(), sf=self.sf, ch_names=self.EEG_CHANNELS, win_sec=win_sec, bands=[(4, 8, 'Theta'), (8, 12, 'Alpha'), (12, 40, 'Beta')]) bandpower = bandpower[self.POWER_BANDS].transpose() bandpower = bandpower.values.reshape(1, -1) # Add feature to vector to LSTM sequence and normalize sequence for prediction. self.sequenceForPrediction = np.roll(self.sequenceForPrediction, -1, axis=1) #Sequence shape (1, timesteps, #features) self.sequenceForPrediction[0, -1, :] = bandpower # Set new data point in last row #normalize sequence normalSequence = (self.sequenceForPrediction - self.global_mean)/self.global_std prediction = self.predictionModel.predict(normalSequence) prediction = prediction[0, 1] # predicted_label = 1 if prediction > 0.5 else 0 # print("prediction scores:", prediction, "label: ", predicted_label) self.lslSender.record_event(prediction) return prediction
# print(timestamps, chunk) chunk = np.array(chunk) # print(chunk.shape) endTime=time.time() # print(endTime - beginTime) beginTime = time.time() #Roll old data to make space for the new chunk dataBuffer = np.roll(dataBuffer, -500, axis=0) #Buffer Shape (Time*SF, channels) chunk = np.array(chunk) #Add chunk in the last 250 rows. 250 samples ==> 1 second. dataBuffer[-500:, :] = chunk[:,:]*1e-6 #Add chunk in the last 250 rows. 250 samples ==> 1 second. # Get bandPower coefficients win_sec = 2 * 0.95 bandpower = yasa.bandpower(dataBuffer[-500:, :].transpose(), sf=sf, ch_names=EEG_channels, win_sec=win_sec, bands=[(0.5, 4, 'Delta'), (4, 8, 'Theta'), (8, 12, 'Alpha'), (12, 30, 'Beta'), (30, 50, 'Gamma')]) # Reshape coefficients into a single row vector bandpower = bandpower[Power_coefficients].values.reshape(1, -1) #Old ######################################### # win = int(1.8 * sf) # Window size for Welch estimation. # freqs, psd = welch(dataBuffer[-500:, :], sf, nperseg=win, axis=0) #Calculate PSD on the last two seconds of data. # # #Calculate the bandpower on 3-D PSD array # bandpower = yasa.bandpower_from_psd_ndarray(psd.transpose(), freqs, bands) #Bandpower shape ==> (#bands, #OfChannels) # bandpower = bandpower.reshape(-1) ########################################## #Add feature to vector to LSTM sequence and normalize sequence for prediction.
feature = [path + i for i in os.listdir(path)] try: os.mkdir(file) except: pass for i, ad in enumerate(feature): try: sf = 128 data = pd.read_csv(ad) data = data / 10 data = data.transpose() # freqs, psd = signal.welch(adhd_data, sf, nperseg=win) a = yasa.bandpower(data.values, sf=sf, ch_names=sensor, relative=False) print(a) #a.loc[:, :'Gamma'].to_csv(f'./{file}/{who}_{i}.csv') """ plt.bar(x=a.loc['T7', :'Gamma'].index, height=a.loc['T7', :'Gamma']) plt.show() plt.bar(x=c.loc['T7', :'Gamma'].index, height=a.loc['T7', :'Gamma']) plt.show() """ except FileNotFoundError: pass else: pass """
def compute_features_stage(raw, hypno, max_freq=35, spindles_params=dict(), sw_params=dict()): """Calculate a set of features for each sleep stage from PSG data. Features are calculated for N2, N3, NREM (= N2 + N3) and REM sleep. Parameters ---------- raw : :py:class:`mne.io.BaseRaw` An MNE Raw instance. hypno : array_like Sleep stage (hypnogram). The hypnogram must have the exact same number of samples as ``data``. To upsample your hypnogram, please refer to :py:func:`yasa.hypno_upsample_to_data`. .. note:: The default hypnogram format in YASA is a 1D integer vector where: - -2 = Unscored - -1 = Artefact / Movement - 0 = Wake - 1 = N1 sleep - 2 = N2 sleep - 3 = N3 sleep - 4 = REM sleep max_freq : int Maximum frequency. This will be used to bandpass-filter the data and to calculate 1 Hz bins bandpower. kwargs_sp : dict Optional keywords arguments that are passed to the :py:func:`yasa.spindles_detect` function. We strongly recommend adapting the thresholds to your population (e.g. more liberal for older adults). kwargs_sw : dict Optional keywords arguments that are passed to the :py:func:`yasa.sw_detect` function. We strongly recommend adapting the thresholds to your population (e.g. more liberal for older adults). """ # ######################################################################### # 2) PREPROCESSING # ######################################################################### # Safety checks assert isinstance(max_freq, int), "`max_freq` must be int." assert isinstance(raw, mne.io.BaseRaw), "`raw` must be a MNE Raw object." assert isinstance(spindles_params, dict) assert isinstance(sw_params, dict) # Define 1 Hz bins frequency bands for bandpower # Similar to [(0.5, 1, "0.5-1"), (1, 2, "1-2"), ..., (34, 35, "34-35")] bands = [] freqs = [0.5] + list(range(1, max_freq + 1)) for i, b in enumerate(freqs[:-1]): bands.append(tuple((b, freqs[i + 1], "%s-%s" % (b, freqs[i + 1])))) # Append traditional bands bands_classic = [(0.5, 1, 'slowdelta'), (1, 4, 'fastdelta'), (0.5, 4, 'delta'), (4, 8, 'theta'), (8, 12, 'alpha'), (12, 16, 'sigma'), (16, 30, 'beta'), (30, max_freq, 'gamma')] bands = bands_classic + bands # Find min and maximum frequencies. These will be used for bandpass-filter # and 1/f adjustement of bandpower. l_freq = 0.5 / h_freq = 35 Hz. all_freqs_sorted = np.sort( np.unique([b[0] for b in bands] + [b[1] for b in bands])) l_freq = all_freqs_sorted[0] h_freq = all_freqs_sorted[-1] # Mapping dictionnary integer to string for sleep stages (2 --> N2) stage_mapping = { -2: 'Unscored', -1: 'Artefact', 0: 'Wake', 1: 'N1', 2: 'N2', 3: 'N3', 4: 'REM', 6: 'NREM', 7: 'WN' # Whole night = N2 + N3 + REM } # Hypnogram check + calculate NREM hypnogram hypno = np.asarray(hypno, dtype=int) assert hypno.ndim == 1, 'Hypno must be one dimensional.' unique_hypno = np.unique(hypno) logger.info('Number of unique values in hypno = %i', unique_hypno.size) # IMPORTANT: NREM is defined as N2 + N3, excluding N1 sleep. hypno_NREM = pd.Series(hypno).replace({2: 6, 3: 6}).to_numpy() minutes_of_NREM = (hypno_NREM == 6).sum() / (60 * raw.info['sfreq']) # WN = Whole night = N2 + N3 + REM (excluding N1) hypno_WN = pd.Series(hypno).replace({2: 7, 3: 7, 4: 7}).to_numpy() # minutes_of_WN = (hypno_WN == 7).sum() / (60 * raw.info['sfreq']) # Keep only EEG channels raw_eeg = raw.pick_types(eeg=True) # Remove suffix from channels: C4-M1 --> C4 chan_nosuffix = [c.split('-')[0] for c in raw_eeg.ch_names] raw_eeg.rename_channels(dict(zip(raw_eeg.ch_names, chan_nosuffix))) # Rename P7/T5 --> P7 chan_noslash = [c.split('/')[0] for c in raw_eeg.ch_names] raw_eeg.rename_channels(dict(zip(raw_eeg.ch_names, chan_noslash))) chan = raw_eeg.ch_names # Resample to 100 Hz and bandpass-filter raw_eeg.resample(100, verbose=False) raw_eeg.filter(l_freq, h_freq, verbose=False) # Extract data and sf data = raw_eeg.get_data() * 1e6 # Scale from Volts (MNE default) to uV sf = raw_eeg.info['sfreq'] assert data.ndim == 2, 'data must be 2D (chan, times).' assert hypno.size == data.shape[1], 'Hypno must have same size as data.' # ######################################################################### # 2) SPECTRAL POWER # ######################################################################### print(" ..calculating spectral powers") # 2.1) 1Hz bins, N2 / N3 / REM # win_sec = 4 sec = 0.25 Hz freq resolution df_bp = yasa.bandpower(raw_eeg, hypno=hypno, bands=bands, win_sec=4, include=(2, 3, 4)) # Same for NREM / WN df_bp_NREM = yasa.bandpower(raw_eeg, hypno=hypno_NREM, bands=bands, include=6) df_bp_WN = yasa.bandpower(raw_eeg, hypno=hypno_WN, bands=bands, include=7) df_bp = df_bp.append(df_bp_NREM).append(df_bp_WN) df_bp.drop(columns=['TotalAbsPow', 'FreqRes', 'Relative'], inplace=True) df_bp = df_bp.add_prefix('bp_').reset_index() # Replace 2 --> N2 df_bp['Stage'] = df_bp['Stage'].map(stage_mapping) # Assert that there are no negative values (see below issue on 1/f) assert not (df_bp._get_numeric_data() < 0).any().any() df_bp.columns = df_bp.columns.str.lower() # 2.2) Same but after adjusting for 1/F (VERY SLOW!) # This is based on the IRASA method described in Wen & Liu 2016. df_bp_1f = [] for stage in [2, 3, 4, 6, 7]: if stage == 6: # Use hypno_NREM data_stage = data[:, hypno_NREM == stage] elif stage == 7: # Use hypno_WN data_stage = data[:, hypno_WN == stage] else: data_stage = data[:, hypno == stage] # Skip if stage is not present in data if data_stage.shape[-1] == 0: continue # Calculate aperiodic / oscillatory PSD + slope freqs, _, psd_osc, fit_params = yasa.irasa(data_stage, sf, ch_names=chan, band=(l_freq, h_freq), win_sec=4) # Make sure that we don't have any negative values in PSD # See https://github.com/raphaelvallat/yasa/issues/29 psd_osc = psd_osc - psd_osc.min(axis=-1, keepdims=True) # Calculate bandpower bp = yasa.bandpower_from_psd(psd_osc, freqs, ch_names=chan, bands=bands) # Add 1/f slope to dataframe and sleep stage bp['1f_slope'] = np.abs(fit_params['Slope'].to_numpy()) bp.insert(loc=0, column="Stage", value=stage_mapping[stage]) df_bp_1f.append(bp) # Convert to a dataframe df_bp_1f = pd.concat(df_bp_1f) # Remove the TotalAbsPower column, incorrect because of negative values df_bp_1f.drop(columns=['TotalAbsPow', 'FreqRes', 'Relative'], inplace=True) df_bp_1f.columns = [ c if c in ['Stage', 'Chan', '1f_slope'] else 'bp_adj_' + c for c in df_bp_1f.columns ] assert not (df_bp_1f._get_numeric_data() < 0).any().any() df_bp_1f.columns = df_bp_1f.columns.str.lower() # ######################################################################### # 3) SPINDLES DETECTION // WORK IN PROGRESS # ######################################################################### # print(" ..detecting sleep spindles") # spindles_params.update(include=(2, 3)) # # Detect spindles in N2 and N3 # # Thresholds have to be tuned with visual scoring of a subset of data # # https://raphaelvallat.com/yasa/build/html/generated/yasa.spindles_detect.html # sp = yasa.spindles_detect(raw_eeg, hypno=hypno, **spindles_params) # df_sp = sp.summary(grp_chan=True, grp_stage=True).reset_index() # df_sp['Stage'] = df_sp['Stage'].map(stage_mapping) # # Aggregate using the mean (adding NREM = N2 + N3) # df_sp = sp.summary(grp_chan=True, grp_stage=True) # df_sp_NREM = sp.summary(grp_chan=True).reset_index() # df_sp_NREM['Stage'] = 6 # df_sp_NREM.set_index(['Stage', 'Channel'], inplace=True) # density_NREM = df_sp_NREM['Count'] / minutes_of_NREM # df_sp_NREM.insert(loc=1, column='Density', # value=density_NREM.to_numpy()) # df_sp = df_sp.append(df_sp_NREM) # df_sp.columns = ['sp_' + c if c in ['Count', 'Density'] else # 'sp_mean_' + c for c in df_sp.columns] # # Prepare to export # df_sp.reset_index(inplace=True) # df_sp['Stage'] = df_sp['Stage'].map(stage_mapping) # df_sp.columns = df_sp.columns.str.lower() # df_sp.rename(columns={'channel': 'chan'}, inplace=True) # ######################################################################### # 4) SLOW-WAVES DETECTION & SW-Sigma COUPLING # ######################################################################### print(" ..detecting slow-waves") # Make sure we calculate coupling sw_params.update(coupling=True) # Detect slow-waves # Option 1: Using absolute thresholds # IMPORTANT: THRESHOLDS MUST BE ADJUSTED ACCORDING TO AGE! sw = yasa.sw_detect(raw_eeg, hypno=hypno, **sw_params) # Aggregate using the mean per channel x stage df_sw = sw.summary(grp_chan=True, grp_stage=True) # Add NREM df_sw_NREM = sw.summary(grp_chan=True).reset_index() df_sw_NREM['Stage'] = 6 df_sw_NREM.set_index(['Stage', 'Channel'], inplace=True) density_NREM = df_sw_NREM['Count'] / minutes_of_NREM df_sw_NREM.insert(loc=1, column='Density', value=density_NREM.to_numpy()) df_sw = df_sw.append(df_sw_NREM)[[ 'Count', 'Density', 'Duration', 'PTP', 'Frequency', 'ndPAC' ]] df_sw.columns = [ 'sw_' + c if c in ['Count', 'Density'] else 'sw_mean_' + c for c in df_sw.columns ] # Aggregate using the coefficient of variation # The CV is a normalized (unitless) standard deviation. Lower # values mean that slow-waves are more similar to each other. # We keep only spefific columns of interest. Not duration because it # is highly correlated with frequency (r=0.99). df_sw_cv = sw.summary(grp_chan=True, grp_stage=True, aggfunc=sp_stats.variation)[[ 'PTP', 'Frequency', 'ndPAC' ]] # Add NREM df_sw_cv_NREM = sw.summary(grp_chan=True, grp_stage=False, aggfunc=sp_stats.variation)[[ 'PTP', 'Frequency', 'ndPAC' ]].reset_index() df_sw_cv_NREM['Stage'] = 6 df_sw_cv_NREM.set_index(['Stage', 'Channel'], inplace=True) df_sw_cv = df_sw_cv.append(df_sw_cv_NREM) df_sw_cv.columns = ['sw_cv_' + c for c in df_sw_cv.columns] # Combine the mean and CV into a single dataframe df_sw = df_sw.join(df_sw_cv).reset_index() df_sw['Stage'] = df_sw['Stage'].map(stage_mapping) df_sw.columns = df_sw.columns.str.lower() df_sw.rename(columns={'channel': 'chan'}, inplace=True) # ######################################################################### # 5) ENTROPY & FRACTAL DIMENSION # ######################################################################### print(" ..calculating entropy measures") # Filter data in the delta band and calculate envelope for CVE data_delta = mne.filter.filter_data(data, sfreq=sf, l_freq=0.5, h_freq=4, l_trans_bandwidth=0.2, h_trans_bandwidth=0.2, verbose=False) env_delta = np.abs(sp_sig.hilbert(data_delta)) # Initialize dataframe idx_ent = pd.MultiIndex.from_product([[2, 3, 4, 6, 7], chan], names=['stage', 'chan']) df_ent = pd.DataFrame(index=idx_ent) for stage in [2, 3, 4, 6, 7]: if stage == 6: # Use hypno_NREM data_stage = data[:, hypno_NREM == stage] env_stage_delta = env_delta[:, hypno_NREM == stage] elif stage == 7: # Use hypno_WN data_stage = data[:, hypno_WN == stage] env_stage_delta = env_delta[:, hypno_WN == stage] else: data_stage = data[:, hypno == stage] env_stage_delta = env_delta[:, hypno == stage] # Skip if stage is not present in data if data_stage.shape[-1] == 0: continue # Entropy and fractal dimension (FD) # See review here: https://pubmed.ncbi.nlm.nih.gov/33286013/ # These are calculated on the broadband signal. # - DFA not implemented because it is dependent on data length. # - Spectral entropy not implemented because the data is not # continuous (segmented by stage). # - Sample / app entropy not implemented because it is too slow to # calculate. from numpy import apply_along_axis as aal df_ent.loc[stage, 'ent_svd'] = aal(ant.svd_entropy, axis=1, arr=data_stage, normalize=True) df_ent.loc[stage, 'ent_perm'] = aal(ant.perm_entropy, axis=1, arr=data_stage, normalize=True) df_ent.loc[stage, 'ent_higuchi'] = aal(ant.higuchi_fd, axis=1, arr=data_stage) # We also add the coefficient of variation of the delta envelope # (CVE), a measure of "slow-wave stability". # See Diaz et al 2018, NeuroImage / Park et al 2021, Sci. Rep. # Lower values = more stable slow-waves (= more sinusoidal) denom = np.sqrt(4 / np.pi - 1) # approx 0.5227 cve = sp_stats.variation(env_stage_delta, axis=1) / denom df_ent.loc[stage, 'ent_cve_delta'] = cve df_ent = df_ent.dropna(how="all").reset_index() df_ent['stage'] = df_ent['stage'].map(stage_mapping) # ######################################################################### # 5) MERGE ALL DATAFRAMES # ######################################################################### df = ( df_bp.merge(df_bp_1f, how='outer') # .merge(df_sp, how='outer') .merge(df_sw, how='outer').merge(df_ent, how='outer')) return df.set_index(['stage', 'chan'])