paradigma 1.0.3__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,64 +1,86 @@
1
- import pandas as pd
2
- import numpy as np
1
+ import json
2
+ import logging
3
3
  from pathlib import Path
4
+
5
+ import numpy as np
6
+ import pandas as pd
4
7
  from scipy import signal
5
8
 
6
9
  from paradigma.classification import ClassifierPackage
10
+ from paradigma.config import IMUConfig, TremorConfig
7
11
  from paradigma.constants import DataColumns
8
- from paradigma.config import TremorConfig
9
- from paradigma.feature_extraction import compute_mfccs, compute_power_in_bandwidth, compute_total_power, extract_frequency_peak, \
10
- extract_tremor_power
11
- from paradigma.segmenting import tabulate_windows, WindowedDataExtractor
12
+ from paradigma.feature_extraction import (
13
+ compute_mfccs,
14
+ compute_power_in_bandwidth,
15
+ compute_total_power,
16
+ extract_frequency_peak,
17
+ extract_tremor_power,
18
+ )
19
+ from paradigma.preprocessing import preprocess_imu_data
20
+ from paradigma.segmenting import WindowedDataExtractor, tabulate_windows
12
21
  from paradigma.util import aggregate_parameter
13
22
 
14
23
 
15
24
  def extract_tremor_features(df: pd.DataFrame, config: TremorConfig) -> pd.DataFrame:
16
25
  """
17
- This function groups sequences of timestamps into windows and subsequently extracts
26
+ This function groups sequences of timestamps into windows and subsequently extracts
18
27
  tremor features from windowed gyroscope data.
19
28
 
20
29
  Parameters
21
30
  ----------
22
31
  df : pd.DataFrame
23
- The input DataFrame containing sensor data, which includes time and gyroscope data. The data should be
24
- structured with the necessary columns as specified in the `config`.
32
+ The input DataFrame containing sensor data, which includes time and
33
+ gyroscope data. The data should be structured with the necessary
34
+ columns as specified in the `config`.
25
35
 
26
36
  config : TremorConfig
27
- Configuration object containing parameters for feature extraction, including column names for time, gyroscope data,
28
- as well as settings for windowing, and feature computation.
37
+ Configuration object containing parameters for feature extraction,
38
+ including column names for time, gyroscope data, as well as settings
39
+ for windowing, and feature computation.
29
40
 
30
41
  Returns
31
42
  -------
32
43
  pd.DataFrame
33
- A DataFrame containing extracted tremor features and a column corresponding to time.
34
-
44
+ A DataFrame containing extracted tremor features and a column
45
+ corresponding to time.
46
+
35
47
  Notes
36
48
  -----
37
49
  - This function groups the data into windows based on timestamps.
38
- - The input DataFrame must include columns as specified in the `config` object for proper feature extraction.
50
+ - The input DataFrame must include columns as specified in the
51
+ `config` object for proper feature extraction.
39
52
 
40
53
  Raises
41
54
  ------
42
55
  ValueError
43
- If the input DataFrame does not contain the required columns as specified in the configuration or if any step in the feature extraction fails.
56
+ If the input DataFrame does not contain the required columns as
57
+ specified in the configuration or if any step in the feature
58
+ extraction fails.
44
59
  """
45
60
  # group sequences of timestamps into windows
46
- windowed_cols = [DataColumns.TIME] + config.gyroscope_cols
47
- windowed_data = tabulate_windows(df, windowed_cols, config.window_length_s, config.window_step_length_s, config.sampling_frequency)
61
+ windowed_colnames = [config.time_colname] + config.gyroscope_colnames
62
+ windowed_data = tabulate_windows(
63
+ df,
64
+ windowed_colnames,
65
+ config.window_length_s,
66
+ config.window_step_length_s,
67
+ config.sampling_frequency,
68
+ )
48
69
 
49
- extractor = WindowedDataExtractor(windowed_cols)
70
+ extractor = WindowedDataExtractor(windowed_colnames)
50
71
 
51
72
  # Extract the start time and gyroscope data from the windowed data
52
- idx_time = extractor.get_index(DataColumns.TIME)
53
- idx_gyro = extractor.get_slice(config.gyroscope_cols)
73
+ idx_time = extractor.get_index(config.time_colname)
74
+ idx_gyro = extractor.get_slice(config.gyroscope_colnames)
54
75
 
55
76
  # Extract data
56
77
  start_time = np.min(windowed_data[:, :, idx_time], axis=1)
57
78
  windowed_gyro = windowed_data[:, :, idx_gyro]
58
79
 
59
- df_features = pd.DataFrame(start_time, columns=[DataColumns.TIME])
60
-
61
- # transform the signals from the temporal domain to the spectral domain and extract tremor features
80
+ df_features = pd.DataFrame(start_time, columns=[config.time_colname])
81
+
82
+ # Transform the signals from the temporal domain to the spectral domain
83
+ # and extract tremor features
62
84
  df_spectral_features = extract_spectral_domain_features(windowed_gyro, config)
63
85
 
64
86
  # Combine spectral features with the start time
@@ -67,50 +89,72 @@ def extract_tremor_features(df: pd.DataFrame, config: TremorConfig) -> pd.DataFr
67
89
  return df_features
68
90
 
69
91
 
70
- def detect_tremor(df: pd.DataFrame, config: TremorConfig, full_path_to_classifier_package: str | Path) -> pd.DataFrame:
92
+ def detect_tremor(
93
+ df: pd.DataFrame, config: TremorConfig, full_path_to_classifier_package: str | Path
94
+ ) -> pd.DataFrame:
71
95
  """
72
- Detects tremor in the input DataFrame using a pre-trained classifier and applies a threshold to the predicted probabilities.
96
+ Detects tremor in the input DataFrame using a pre-trained classifier and
97
+ applies a threshold to the predicted probabilities.
73
98
 
74
99
  This function performs the following steps:
75
- 1. Loads the pre-trained classifier and scaling parameters from the provided directory.
76
- 2. Scales the relevant features in the input DataFrame (`df`) using the loaded scaling parameters.
77
- 3. Makes predictions using the classifier to estimate the probability of tremor.
78
- 4. Applies a threshold to the predicted probabilities to classify whether tremor is detected or not.
79
- 5. Checks for rest tremor by verifying the frequency of the peak and below tremor power.
80
- 6. Adds the predicted probabilities and the classification result to the DataFrame.
100
+ 1. Loads the pre-trained classifier and scaling parameters from the
101
+ provided directory.
102
+ 2. Scales the relevant features in the input DataFrame (`df`) using the
103
+ loaded scaling parameters.
104
+ 3. Makes predictions using the classifier to estimate the probability of
105
+ tremor.
106
+ 4. Applies a threshold to the predicted probabilities to classify whether
107
+ tremor is detected or not.
108
+ 5. Checks for rest tremor by verifying the frequency of the peak and
109
+ below tremor power.
110
+ 6. Adds the predicted probabilities and the classification result to the
111
+ DataFrame.
81
112
 
82
113
  Parameters
83
114
  ----------
84
115
  df : pd.DataFrame
85
- The input DataFrame containing extracted tremor features. The DataFrame must include
86
- the necessary columns as specified in the classifier's feature names.
116
+ The input DataFrame containing extracted tremor features. The
117
+ DataFrame must include the necessary columns as specified in the
118
+ classifier's feature names.
87
119
 
88
120
  config : TremorConfig
89
- Configuration object containing settings for tremor detection, including the frequency range for rest tremor.
121
+ Configuration object containing settings for tremor detection,
122
+ including the frequency range for rest tremor.
90
123
 
91
124
  full_path_to_classifier_package : str | Path
92
- The path to the directory containing the classifier file, threshold value, scaler parameters, and other necessary input
93
- files for tremor detection.
125
+ The path to the directory containing the classifier file, threshold
126
+ value, scaler parameters, and other necessary input files for tremor
127
+ detection.
94
128
 
95
129
  Returns
96
130
  -------
97
131
  pd.DataFrame
98
132
  The input DataFrame (`df`) with two additional columns:
99
- - `PRED_TREMOR_PROBA`: Predicted probability of tremor based on the classifier.
100
- - `PRED_TREMOR_LOGREG`: Binary classification result (True for tremor, False for no tremor), based on the threshold applied to `PRED_TREMOR_PROBA`.
101
- - `PRED_TREMOR_CHECKED`: Binary classification result (True for tremor, False for no tremor), after performing extra checks for rest tremor on `PRED_TREMOR_LOGREG`.
102
- - `PRED_ARM_AT_REST`: Binary classification result (True for arm at rest or stable posture, False for significant arm movement), based on the power below tremor.
133
+ - `PRED_TREMOR_PROBA`: Predicted probability of tremor based on the
134
+ classifier.
135
+ - `PRED_TREMOR_LOGREG`: Binary classification result (True for tremor,
136
+ False for no tremor), based on the threshold applied to
137
+ `PRED_TREMOR_PROBA`.
138
+ - `PRED_TREMOR_CHECKED`: Binary classification result (True for
139
+ tremor, False for no tremor), after performing extra checks for
140
+ rest tremor on `PRED_TREMOR_LOGREG`.
141
+ - `PRED_ARM_AT_REST`: Binary classification result (True for arm at
142
+ rest or stable posture, False for significant arm movement), based
143
+ on the power below tremor.
103
144
 
104
145
  Notes
105
146
  -----
106
- - The threshold used to classify tremor is loaded from a file and applied to the predicted probabilities.
147
+ - The threshold used to classify tremor is loaded from a file and
148
+ applied to the predicted probabilities.
107
149
 
108
150
  Raises
109
151
  ------
110
152
  FileNotFoundError
111
- If the classifier, scaler, or threshold files are not found at the specified paths.
153
+ If the classifier, scaler, or threshold files are not found at the
154
+ specified paths.
112
155
  ValueError
113
- If the DataFrame does not contain the expected features for prediction or if the prediction fails.
156
+ If the DataFrame does not contain the expected features for
157
+ prediction or if the prediction fails.
114
158
 
115
159
  """
116
160
 
@@ -126,96 +170,130 @@ def detect_tremor(df: pd.DataFrame, config: TremorConfig, full_path_to_classifie
126
170
  scaled_features = clf_package.transform_features(df.loc[:, feature_names_scaling])
127
171
 
128
172
  # Replace scaled features in a copy of the relevant features for prediction
129
- X = df.loc[:, feature_names_predictions].copy()
130
- X.loc[:, feature_names_scaling] = scaled_features
173
+ x_features = df.loc[:, feature_names_predictions].copy()
174
+ x_features.loc[:, feature_names_scaling] = scaled_features
131
175
 
132
- # Get the tremor probability
133
- df[DataColumns.PRED_TREMOR_PROBA] = clf_package.predict_proba(X)
176
+ # Get the tremor probability
177
+ df[DataColumns.PRED_TREMOR_PROBA] = clf_package.predict_proba(x_features)
134
178
 
135
179
  # Make prediction based on pre-defined threshold
136
- df[DataColumns.PRED_TREMOR_LOGREG] = (df[DataColumns.PRED_TREMOR_PROBA] >= clf_package.threshold).astype(int)
180
+ df[DataColumns.PRED_TREMOR_LOGREG] = (
181
+ df[DataColumns.PRED_TREMOR_PROBA] >= clf_package.threshold
182
+ ).astype(int)
183
+
184
+ # Perform extra checks for rest tremor
185
+ peak_check = (df[DataColumns.FREQ_PEAK] >= config.fmin_rest_tremor) & (
186
+ df[DataColumns.FREQ_PEAK] <= config.fmax_rest_tremor
187
+ ) # peak within 3-7 Hz
188
+ df[DataColumns.PRED_ARM_AT_REST] = (
189
+ df[DataColumns.BELOW_TREMOR_POWER] <= config.movement_threshold
190
+ ).astype(
191
+ int
192
+ ) # arm at rest or in stable posture
193
+ df[DataColumns.PRED_TREMOR_CHECKED] = (
194
+ (df[DataColumns.PRED_TREMOR_LOGREG] == 1)
195
+ & peak_check
196
+ & df[DataColumns.PRED_ARM_AT_REST]
197
+ ).astype(int)
137
198
 
138
- # Perform extra checks for rest tremor
139
- peak_check = (df['freq_peak'] >= config.fmin_rest_tremor) & (df['freq_peak']<=config.fmax_rest_tremor) # peak within 3-7 Hz
140
- df[DataColumns.PRED_ARM_AT_REST] = (df['below_tremor_power'] <= config.movement_threshold).astype(int) # arm at rest or in stable posture
141
- df[DataColumns.PRED_TREMOR_CHECKED] = ((df[DataColumns.PRED_TREMOR_LOGREG]==1) & (peak_check==True) & (df[DataColumns.PRED_ARM_AT_REST] == True)).astype(int)
142
-
143
199
  return df
144
200
 
201
+
145
202
  def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
146
203
  """
147
- Quantifies the amount of tremor time and tremor power, aggregated over all windows in the input dataframe.
148
- Tremor time is calculated as the number of the detected tremor windows, as percentage of the number of windows
149
- without significant non-tremor movement (at rest). For tremor power the following aggregates are derived:
150
- the median, mode and percentile of tremor power specified in the configuration object.
151
-
204
+ Quantifies the amount of tremor time and tremor power, aggregated over
205
+ all windows in the input dataframe. Tremor time is calculated as the
206
+ number of the detected tremor windows, as percentage of the number of
207
+ windows without significant non-tremor movement (at rest). For tremor
208
+ power the following aggregates are derived: the median, mode and
209
+ percentile of tremor power specified in the configuration object.
210
+
152
211
  Parameters
153
212
  ----------
154
213
  df : pd.DataFrame
155
- The input DataFrame containing the tremor predictions and computed tremor power.
156
- The DataFrame must also contain a datatime column ('time_dt').
214
+ The input DataFrame containing the tremor predictions and computed
215
+ tremor power. The DataFrame must also contain a datatime column
216
+ ('time_dt').
157
217
 
158
218
  config : TremorConfig
159
- Configuration object containing the percentile for aggregating tremor power.
219
+ Configuration object containing the percentile for aggregating tremor
220
+ power.
160
221
 
161
222
  Returns
162
223
  -------
163
224
  dict
164
- A dictionary with the aggregated tremor time and tremor power measures, as well as the number of valid days,
165
- the total number of windows, and the number of windows at rest available in the input dataframe.
225
+ A dictionary with the aggregated tremor time and tremor power
226
+ measures, as well as the number of valid days, the total number of
227
+ windows, and the number of windows at rest available in the input
228
+ dataframe.
166
229
 
167
230
  Notes
168
231
  -----
169
- - Tremor power is converted to log scale, after adding a constant of 1, so that zero tremor power
170
- corresponds to a value of 0 in log scale.
171
- - The modal tremor power is computed based on gaussian kernel density estimation.
172
-
232
+ - Tremor power is converted to log scale, after adding a constant of
233
+ 1, so that zero tremor power corresponds to a value of 0 in log
234
+ scale.
235
+ - The modal tremor power is computed based on gaussian kernel density
236
+ estimation.
237
+
173
238
  """
174
- nr_valid_days = df['time_dt'].dt.date.unique().size # number of valid days in the input dataframe
175
- nr_windows_total = df.shape[0] # number of windows in the input dataframe
239
+ nr_valid_days = (
240
+ df["time_dt"].dt.date.unique().size
241
+ ) # number of valid days in the input dataframe
242
+ nr_windows_total = df.shape[0] # number of windows in the input dataframe
176
243
 
177
- # remove windows with detected non-tremor arm movements to control for the amount of arm activities performed
244
+ # Remove windows with detected non-tremor arm movements to control for
245
+ # the amount of arm activities performed
178
246
  df_filtered = df.loc[df.pred_arm_at_rest == 1]
179
- nr_windows_rest = df_filtered.shape[0] # number of windows without non-tremor arm movement
247
+ nr_windows_rest = df_filtered.shape[
248
+ 0
249
+ ] # number of windows without non-tremor arm movement
180
250
 
181
- if nr_windows_rest == 0: # if no windows without non-tremor arm movement are detected
182
- raise Warning('No windows without non-tremor arm movement are detected.')
251
+ if (
252
+ nr_windows_rest == 0
253
+ ): # if no windows without non-tremor arm movement are detected
254
+ raise Warning("No windows without non-tremor arm movement are detected.")
183
255
 
184
- # calculate tremor time
185
- n_windows_tremor = np.sum(df_filtered['pred_tremor_checked'])
186
- perc_windows_tremor = n_windows_tremor / nr_windows_rest * 100 # as percentage of total measured time without non-tremor arm movement
256
+ # Calculate tremor time
257
+ n_windows_tremor = np.sum(df_filtered[DataColumns.PRED_TREMOR_CHECKED])
258
+ # As percentage of total measured time without non-tremor arm movement
259
+ perc_windows_tremor = n_windows_tremor / nr_windows_rest * 100
187
260
 
188
- aggregated_tremor_power = {} # initialize dictionary to store aggregated tremor power measures
189
-
190
- if n_windows_tremor == 0: # if no tremor is detected, the tremor power measures are set to NaN
261
+ # Initialize dictionary to store aggregated tremor power measures
262
+ aggregated_tremor_power = {}
191
263
 
192
- aggregated_tremor_power['median_tremor_power'] = np.nan
193
- aggregated_tremor_power['mode_binned_tremor_power'] = np.nan
194
- aggregated_tremor_power['90p_tremor_power'] = np.nan
264
+ # If no tremor is detected, the tremor power measures are set to NaN
265
+ if n_windows_tremor == 0:
266
+ aggregated_tremor_power["median_tremor_power"] = np.nan
267
+ aggregated_tremor_power["mode_binned_tremor_power"] = np.nan
268
+ aggregated_tremor_power["90p_tremor_power"] = np.nan
195
269
 
196
270
  else:
197
-
271
+
198
272
  # calculate aggregated tremor power measures
199
- tremor_power = df_filtered.loc[df_filtered['pred_tremor_checked'] == 1, 'tremor_power']
200
- tremor_power = np.log10(tremor_power+1) # convert to log scale
201
-
273
+ tremor_power = df_filtered.loc[
274
+ df_filtered[DataColumns.PRED_TREMOR_CHECKED] == 1, DataColumns.TREMOR_POWER
275
+ ]
276
+ tremor_power = np.log10(tremor_power + 1) # convert to log scale
277
+
202
278
  for aggregate in config.aggregates_tremor_power:
203
279
  aggregate_name = f"{aggregate}_tremor_power"
204
- aggregated_tremor_power[aggregate_name] = aggregate_parameter(tremor_power, aggregate, config.evaluation_points_tremor_power)
280
+ aggregated_tremor_power[aggregate_name] = aggregate_parameter(
281
+ tremor_power, aggregate, config.evaluation_points_tremor_power
282
+ )
205
283
 
206
284
  # store aggregates in json format
207
285
  d_aggregates = {
208
- 'metadata': {
209
- 'nr_valid_days': nr_valid_days,
210
- 'nr_windows_total': nr_windows_total,
211
- 'nr_windows_rest': nr_windows_rest
286
+ "metadata": {
287
+ "nr_valid_days": nr_valid_days,
288
+ "nr_windows_total": nr_windows_total,
289
+ "nr_windows_rest": nr_windows_rest,
290
+ },
291
+ "aggregated_tremor_measures": {
292
+ "perc_windows_tremor": perc_windows_tremor,
293
+ "median_tremor_power": aggregated_tremor_power["median_tremor_power"],
294
+ "modal_tremor_power": aggregated_tremor_power["mode_binned_tremor_power"],
295
+ "90p_tremor_power": aggregated_tremor_power["90p_tremor_power"],
212
296
  },
213
- 'aggregated_tremor_measures': {
214
- 'perc_windows_tremor': perc_windows_tremor,
215
- 'median_tremor_power': aggregated_tremor_power['median_tremor_power'],
216
- 'modal_tremor_power': aggregated_tremor_power['mode_binned_tremor_power'],
217
- '90p_tremor_power': aggregated_tremor_power['90p_tremor_power']
218
- }
219
297
  }
220
298
 
221
299
  return d_aggregates
@@ -225,23 +303,26 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
225
303
  """
226
304
  Compute spectral domain features from the gyroscope data.
227
305
 
228
- This function computes Mel-frequency cepstral coefficients (MFCCs), the frequency of the peak,
229
- the tremor power, and the below tremor power based on the total power spectral density of the windowed gyroscope data.
306
+ This function computes Mel-frequency cepstral coefficients (MFCCs), the
307
+ frequency of the peak, the tremor power, and the below tremor power
308
+ based on the total power spectral density of the windowed gyroscope
309
+ data.
230
310
 
231
311
  Parameters
232
312
  ----------
233
313
  data : numpy.ndarray
234
314
  A 2D numpy array where each row corresponds to a window of gyroscope data.
235
315
  config : object
236
- Configuration object containing settings such as sampling frequency, window type,
237
- and MFCC parameters.
238
-
316
+ Configuration object containing settings such as sampling frequency,
317
+ window type, and MFCC parameters.
318
+
239
319
  Returns
240
320
  -------
241
321
  pd.DataFrame
242
- The feature dataframe containing the extracted spectral features, including
243
- MFCCs, the frequency of the peak, the tremor power and below tremor power for each window.
244
-
322
+ The feature dataframe containing the extracted spectral features,
323
+ including MFCCs, the frequency of the peak, the tremor power and
324
+ below tremor power for each window.
325
+
245
326
  """
246
327
 
247
328
  # Initialize a dictionary to hold the results
@@ -253,7 +334,7 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
253
334
  segment_length_spectrogram_s = config.segment_length_spectrogram_s
254
335
  overlap_fraction = config.overlap_fraction
255
336
  spectral_resolution = config.spectral_resolution
256
- window_type = 'hann'
337
+ window_type = "hann"
257
338
 
258
339
  # Compute the power spectral density
259
340
  segment_length_n = sampling_frequency * segment_length_psd_s
@@ -262,15 +343,15 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
262
343
  nfft = sampling_frequency / spectral_resolution
263
344
 
264
345
  freqs, psd = signal.welch(
265
- x=data,
266
- fs=sampling_frequency,
267
- window=window,
346
+ x=data,
347
+ fs=sampling_frequency,
348
+ window=window,
268
349
  nperseg=segment_length_n,
269
- noverlap=overlap_n,
270
- nfft=nfft,
271
- detrend=False,
272
- scaling='density',
273
- axis=1
350
+ noverlap=overlap_n,
351
+ nfft=nfft,
352
+ detrend=False,
353
+ scaling="density",
354
+ axis=1,
274
355
  )
275
356
 
276
357
  # Compute the spectrogram
@@ -278,19 +359,20 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
278
359
  overlap_n = segment_length_n * overlap_fraction
279
360
  window = signal.get_window(window_type, segment_length_n)
280
361
 
281
- f, t, S1 = signal.stft(
282
- x=data,
283
- fs=sampling_frequency,
284
- window=window,
285
- nperseg=segment_length_n,
362
+ f, t, stft_result = signal.stft(
363
+ x=data,
364
+ fs=sampling_frequency,
365
+ window=window,
366
+ nperseg=segment_length_n,
286
367
  noverlap=overlap_n,
287
368
  boundary=None,
288
- axis=1
369
+ axis=1,
289
370
  )
290
371
 
291
- # Compute total power in the PSD and the total spectrogram (summed over the three axes)
372
+ # Compute total power in the PSD and the total spectrogram (summed over
373
+ # the three axes)
292
374
  total_psd = compute_total_power(psd)
293
- total_spectrogram = np.sum(np.abs(S1)*sampling_frequency, axis=2)
375
+ total_spectrogram = np.sum(np.abs(stft_result) * sampling_frequency, axis=2)
294
376
 
295
377
  # Compute the MFCC's
296
378
  config.mfcc_low_frequency = config.fmin_mfcc
@@ -301,21 +383,212 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
301
383
  mfccs = compute_mfccs(
302
384
  total_power_array=total_spectrogram,
303
385
  config=config,
304
- total_power_type='spectrogram',
305
- rounding_method='round',
306
- multiplication_factor=1
386
+ total_power_type="spectrogram",
387
+ rounding_method="round",
388
+ multiplication_factor=1,
307
389
  )
308
390
 
309
391
  # Combine the MFCCs into the features DataFrame
310
- mfcc_colnames = [f'mfcc_{x}' for x in range(1, config.mfcc_n_coefficients + 1)]
392
+ mfcc_colnames = [f"mfcc_{x}" for x in range(1, config.mfcc_n_coefficients + 1)]
311
393
  for i, colname in enumerate(mfcc_colnames):
312
394
  feature_dict[colname] = mfccs[:, i]
313
395
 
314
396
  # Compute the frequency of the peak, non-tremor power and tremor power
315
- feature_dict['freq_peak'] = extract_frequency_peak(freqs, total_psd, config.fmin_peak_search, config.fmax_peak_search)
316
- feature_dict['below_tremor_power'] = compute_power_in_bandwidth(freqs, total_psd, config.fmin_below_rest_tremor, config.fmax_below_rest_tremor,
317
- include_max=False, spectral_resolution=config.spectral_resolution,
318
- cumulative_sum_method='sum')
319
- feature_dict['tremor_power'] = extract_tremor_power(freqs, total_psd, config.fmin_rest_tremor, config.fmax_rest_tremor)
397
+ feature_dict[DataColumns.FREQ_PEAK] = extract_frequency_peak(
398
+ freqs, total_psd, config.fmin_peak_search, config.fmax_peak_search
399
+ )
400
+ feature_dict[DataColumns.BELOW_TREMOR_POWER] = compute_power_in_bandwidth(
401
+ freqs,
402
+ total_psd,
403
+ config.fmin_below_rest_tremor,
404
+ config.fmax_below_rest_tremor,
405
+ include_max=False,
406
+ spectral_resolution=config.spectral_resolution,
407
+ cumulative_sum_method="sum",
408
+ )
409
+ feature_dict[DataColumns.TREMOR_POWER] = extract_tremor_power(
410
+ freqs, total_psd, config.fmin_rest_tremor, config.fmax_rest_tremor
411
+ )
412
+
413
+ return pd.DataFrame(feature_dict)
414
+
415
+
416
+ def run_tremor_pipeline(
417
+ df_prepared: pd.DataFrame,
418
+ output_dir: str | Path,
419
+ store_intermediate: list[str] = [],
420
+ tremor_config: TremorConfig | None = None,
421
+ imu_config: IMUConfig | None = None,
422
+ logging_level: int = logging.INFO,
423
+ custom_logger: logging.Logger | None = None,
424
+ ) -> pd.DataFrame:
425
+ """
426
+ High-level tremor analysis pipeline for a single segment.
427
+
428
+ This function implements the complete tremor analysis workflow from the
429
+ tremor tutorial:
430
+ 1. Preprocess gyroscope data
431
+ 2. Extract tremor features
432
+ 3. Detect tremor
433
+ 4. Quantify tremor (select relevant columns)
434
+
435
+ Parameters
436
+ ----------
437
+ df_prepared : pd.DataFrame
438
+ Prepared sensor data with time and gyroscope columns
439
+ output_dir : str or Path
440
+ Output directory for intermediate results (required)
441
+ store_intermediate : list of str, default []
442
+ Which intermediate results to store
443
+ tremor_config : TremorConfig, optional
444
+ Tremor analysis configuration
445
+ imu_config : IMUConfig, optional
446
+ IMU preprocessing configuration
447
+ logging_level : int, default logging.INFO
448
+ Logging level using standard logging constants
449
+ custom_logger : logging.Logger, optional
450
+ Custom logger instance
451
+
452
+ Returns
453
+ -------
454
+ pd.DataFrame
455
+ Quantified tremor data with columns:
456
+ - time: timestamp
457
+ - pred_arm_at_rest: arm at rest prediction
458
+ - pred_tremor_checked: tremor detection result
459
+ - tremor_power: tremor power measure
460
+
461
+ """
462
+ # Setup logger
463
+ active_logger = (
464
+ custom_logger if custom_logger is not None else logging.getLogger(__name__)
465
+ )
466
+ if custom_logger is None:
467
+ active_logger.setLevel(logging_level)
468
+
469
+ if tremor_config is None:
470
+ tremor_config = TremorConfig()
471
+ if imu_config is None:
472
+ imu_config = IMUConfig()
473
+
474
+ output_dir = Path(output_dir)
475
+
476
+ # Validate input data columns
477
+ required_columns = [
478
+ DataColumns.TIME,
479
+ DataColumns.GYROSCOPE_X,
480
+ DataColumns.GYROSCOPE_Y,
481
+ DataColumns.GYROSCOPE_Z,
482
+ ]
483
+ missing_columns = [
484
+ col for col in required_columns if col not in df_prepared.columns
485
+ ]
486
+ if missing_columns:
487
+ active_logger.warning(
488
+ f"Missing required columns for tremor pipeline: " f"{missing_columns}"
489
+ )
490
+ return pd.DataFrame()
491
+
492
+ # Step 1: Preprocess gyroscope data (following tutorial)
493
+ active_logger.info("Step 1: Preprocessing gyroscope data")
494
+ df_preprocessed = preprocess_imu_data(
495
+ df_prepared,
496
+ imu_config,
497
+ sensor="gyroscope",
498
+ watch_side="left", # Watch side is unimportant for tremor detection
499
+ verbose=1 if logging_level <= logging.INFO else 0,
500
+ )
501
+
502
+ if "preprocessing" in store_intermediate:
503
+ preprocessing_dir = output_dir / "preprocessing"
504
+ preprocessing_dir.mkdir(exist_ok=True)
505
+ df_preprocessed.to_parquet(preprocessing_dir / "tremor_preprocessed.parquet")
506
+ active_logger.info(f"Saved preprocessed data to {preprocessing_dir}")
507
+
508
+ # Step 2: Extract tremor features
509
+ active_logger.info("Step 2: Extracting tremor features")
510
+ df_features = extract_tremor_features(df_preprocessed, tremor_config)
511
+
512
+ if "tremor" in store_intermediate:
513
+ tremor_dir = output_dir / "tremor"
514
+ tremor_dir.mkdir(exist_ok=True)
515
+ df_features.to_parquet(tremor_dir / "tremor_features.parquet")
516
+ active_logger.info(f"Saved tremor features to {tremor_dir}")
517
+
518
+ # Step 3: Detect tremor
519
+ active_logger.info("Step 3: Detecting tremor")
520
+ try:
521
+ from importlib.resources import files
522
+
523
+ classifier_path = files("paradigma.assets") / "tremor_detection_clf_package.pkl"
524
+ df_predictions = detect_tremor(df_features, tremor_config, classifier_path)
525
+ except Exception as e:
526
+ active_logger.error(f"Tremor detection failed: {e}")
527
+ return pd.DataFrame()
528
+
529
+ # Step 4: Quantify tremor (following tutorial pattern)
530
+ active_logger.info("Step 4: Quantifying tremor")
531
+
532
+ # Select quantification columns as in the tutorial
533
+ quantification_columns = [
534
+ tremor_config.time_colname,
535
+ DataColumns.PRED_ARM_AT_REST,
536
+ DataColumns.PRED_TREMOR_CHECKED,
537
+ DataColumns.TREMOR_POWER,
538
+ ]
539
+
540
+ # Check if all required columns exist
541
+ available_columns = [
542
+ col for col in quantification_columns if col in df_predictions.columns
543
+ ]
544
+ if len(available_columns) != len(quantification_columns):
545
+ missing = set(quantification_columns) - set(available_columns)
546
+ active_logger.warning(f"Missing quantification columns: {missing}")
547
+ # Use available columns
548
+ quantification_columns = available_columns
549
+
550
+ df_quantification = df_predictions[quantification_columns].copy()
551
+
552
+ # Set tremor power to None for non-tremor windows (following tutorial)
553
+ if (
554
+ DataColumns.TREMOR_POWER in df_quantification.columns
555
+ and DataColumns.PRED_TREMOR_CHECKED in df_quantification.columns
556
+ ):
557
+ df_quantification.loc[
558
+ df_quantification[DataColumns.PRED_TREMOR_CHECKED] == 0,
559
+ DataColumns.TREMOR_POWER,
560
+ ] = None
561
+
562
+ if "quantification" in store_intermediate:
563
+ quantification_dir = output_dir / "quantification"
564
+ quantification_dir.mkdir(exist_ok=True)
565
+ df_quantification.to_parquet(
566
+ quantification_dir / "tremor_quantification.parquet"
567
+ )
568
+
569
+ # Save quantification metadata
570
+ quantification_meta = {
571
+ "total_windows": len(df_quantification),
572
+ "tremor_windows": (
573
+ int(df_quantification[DataColumns.PRED_TREMOR_CHECKED].sum())
574
+ if DataColumns.PRED_TREMOR_CHECKED in df_quantification.columns
575
+ else 0
576
+ ),
577
+ "columns": list(df_quantification.columns),
578
+ }
579
+ with open(quantification_dir / "tremor_quantification_meta.json", "w") as f:
580
+ json.dump(quantification_meta, f, indent=2)
581
+
582
+ active_logger.debug(f"Saved tremor quantification to {quantification_dir}")
583
+
584
+ tremor_windows = (
585
+ int(df_quantification[DataColumns.PRED_TREMOR_CHECKED].sum())
586
+ if DataColumns.PRED_TREMOR_CHECKED in df_quantification.columns
587
+ else 0
588
+ )
589
+ active_logger.info(
590
+ f"Tremor analysis completed: {tremor_windows} tremor windows "
591
+ f"detected from {len(df_quantification)} total windows"
592
+ )
320
593
 
321
- return pd.DataFrame(feature_dict)
594
+ return df_quantification