paradigma 1.0.4__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.
- paradigma/__init__.py +10 -1
- paradigma/classification.py +14 -14
- paradigma/config.py +38 -29
- paradigma/constants.py +10 -2
- paradigma/feature_extraction.py +106 -75
- paradigma/load.py +476 -0
- paradigma/orchestrator.py +670 -0
- paradigma/pipelines/gait_pipeline.py +488 -97
- paradigma/pipelines/pulse_rate_pipeline.py +278 -46
- paradigma/pipelines/pulse_rate_utils.py +176 -137
- paradigma/pipelines/tremor_pipeline.py +292 -72
- paradigma/prepare_data.py +409 -0
- paradigma/preprocessing.py +345 -77
- paradigma/segmenting.py +57 -42
- paradigma/testing.py +14 -9
- paradigma/util.py +36 -22
- paradigma-1.1.0.dist-info/METADATA +229 -0
- paradigma-1.1.0.dist-info/RECORD +26 -0
- {paradigma-1.0.4.dist-info → paradigma-1.1.0.dist-info}/WHEEL +1 -1
- paradigma-1.0.4.dist-info/METADATA +0 -140
- paradigma-1.0.4.dist-info/RECORD +0 -23
- {paradigma-1.0.4.dist-info → paradigma-1.1.0.dist-info}/entry_points.txt +0 -0
- {paradigma-1.0.4.dist-info → paradigma-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
|
|
3
5
|
import numpy as np
|
|
@@ -5,7 +7,7 @@ import pandas as pd
|
|
|
5
7
|
from scipy import signal
|
|
6
8
|
|
|
7
9
|
from paradigma.classification import ClassifierPackage
|
|
8
|
-
from paradigma.config import TremorConfig
|
|
10
|
+
from paradigma.config import IMUConfig, TremorConfig
|
|
9
11
|
from paradigma.constants import DataColumns
|
|
10
12
|
from paradigma.feature_extraction import (
|
|
11
13
|
compute_mfccs,
|
|
@@ -14,6 +16,7 @@ from paradigma.feature_extraction import (
|
|
|
14
16
|
extract_frequency_peak,
|
|
15
17
|
extract_tremor_power,
|
|
16
18
|
)
|
|
19
|
+
from paradigma.preprocessing import preprocess_imu_data
|
|
17
20
|
from paradigma.segmenting import WindowedDataExtractor, tabulate_windows
|
|
18
21
|
from paradigma.util import aggregate_parameter
|
|
19
22
|
|
|
@@ -26,27 +29,33 @@ def extract_tremor_features(df: pd.DataFrame, config: TremorConfig) -> pd.DataFr
|
|
|
26
29
|
Parameters
|
|
27
30
|
----------
|
|
28
31
|
df : pd.DataFrame
|
|
29
|
-
The input DataFrame containing sensor data, which includes time and
|
|
30
|
-
|
|
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`.
|
|
31
35
|
|
|
32
36
|
config : TremorConfig
|
|
33
|
-
Configuration object containing parameters for feature extraction,
|
|
34
|
-
|
|
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.
|
|
35
40
|
|
|
36
41
|
Returns
|
|
37
42
|
-------
|
|
38
43
|
pd.DataFrame
|
|
39
|
-
A DataFrame containing extracted tremor features and a column
|
|
44
|
+
A DataFrame containing extracted tremor features and a column
|
|
45
|
+
corresponding to time.
|
|
40
46
|
|
|
41
47
|
Notes
|
|
42
48
|
-----
|
|
43
49
|
- This function groups the data into windows based on timestamps.
|
|
44
|
-
- The input DataFrame must include columns as specified in the
|
|
50
|
+
- The input DataFrame must include columns as specified in the
|
|
51
|
+
`config` object for proper feature extraction.
|
|
45
52
|
|
|
46
53
|
Raises
|
|
47
54
|
------
|
|
48
55
|
ValueError
|
|
49
|
-
If the input DataFrame does not contain the required columns as
|
|
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.
|
|
50
59
|
"""
|
|
51
60
|
# group sequences of timestamps into windows
|
|
52
61
|
windowed_colnames = [config.time_colname] + config.gyroscope_colnames
|
|
@@ -70,7 +79,8 @@ def extract_tremor_features(df: pd.DataFrame, config: TremorConfig) -> pd.DataFr
|
|
|
70
79
|
|
|
71
80
|
df_features = pd.DataFrame(start_time, columns=[config.time_colname])
|
|
72
81
|
|
|
73
|
-
#
|
|
82
|
+
# Transform the signals from the temporal domain to the spectral domain
|
|
83
|
+
# and extract tremor features
|
|
74
84
|
df_spectral_features = extract_spectral_domain_features(windowed_gyro, config)
|
|
75
85
|
|
|
76
86
|
# Combine spectral features with the start time
|
|
@@ -83,48 +93,68 @@ def detect_tremor(
|
|
|
83
93
|
df: pd.DataFrame, config: TremorConfig, full_path_to_classifier_package: str | Path
|
|
84
94
|
) -> pd.DataFrame:
|
|
85
95
|
"""
|
|
86
|
-
Detects tremor in the input DataFrame using a pre-trained classifier and
|
|
96
|
+
Detects tremor in the input DataFrame using a pre-trained classifier and
|
|
97
|
+
applies a threshold to the predicted probabilities.
|
|
87
98
|
|
|
88
99
|
This function performs the following steps:
|
|
89
|
-
1. Loads the pre-trained classifier and scaling parameters from the
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
95
112
|
|
|
96
113
|
Parameters
|
|
97
114
|
----------
|
|
98
115
|
df : pd.DataFrame
|
|
99
|
-
The input DataFrame containing extracted tremor features. The
|
|
100
|
-
the necessary columns as specified in the
|
|
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.
|
|
101
119
|
|
|
102
120
|
config : TremorConfig
|
|
103
|
-
Configuration object containing settings for tremor detection,
|
|
121
|
+
Configuration object containing settings for tremor detection,
|
|
122
|
+
including the frequency range for rest tremor.
|
|
104
123
|
|
|
105
124
|
full_path_to_classifier_package : str | Path
|
|
106
|
-
The path to the directory containing the classifier file, threshold
|
|
107
|
-
files for tremor
|
|
125
|
+
The path to the directory containing the classifier file, threshold
|
|
126
|
+
value, scaler parameters, and other necessary input files for tremor
|
|
127
|
+
detection.
|
|
108
128
|
|
|
109
129
|
Returns
|
|
110
130
|
-------
|
|
111
131
|
pd.DataFrame
|
|
112
132
|
The input DataFrame (`df`) with two additional columns:
|
|
113
|
-
- `PRED_TREMOR_PROBA`: Predicted probability of tremor based on the
|
|
114
|
-
|
|
115
|
-
- `
|
|
116
|
-
|
|
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.
|
|
117
144
|
|
|
118
145
|
Notes
|
|
119
146
|
-----
|
|
120
|
-
- The threshold used to classify tremor is loaded from a file and
|
|
147
|
+
- The threshold used to classify tremor is loaded from a file and
|
|
148
|
+
applied to the predicted probabilities.
|
|
121
149
|
|
|
122
150
|
Raises
|
|
123
151
|
------
|
|
124
152
|
FileNotFoundError
|
|
125
|
-
If the classifier, scaler, or threshold files are not found at the
|
|
153
|
+
If the classifier, scaler, or threshold files are not found at the
|
|
154
|
+
specified paths.
|
|
126
155
|
ValueError
|
|
127
|
-
If the DataFrame does not contain the expected features for
|
|
156
|
+
If the DataFrame does not contain the expected features for
|
|
157
|
+
prediction or if the prediction fails.
|
|
128
158
|
|
|
129
159
|
"""
|
|
130
160
|
|
|
@@ -140,11 +170,11 @@ def detect_tremor(
|
|
|
140
170
|
scaled_features = clf_package.transform_features(df.loc[:, feature_names_scaling])
|
|
141
171
|
|
|
142
172
|
# Replace scaled features in a copy of the relevant features for prediction
|
|
143
|
-
|
|
144
|
-
|
|
173
|
+
x_features = df.loc[:, feature_names_predictions].copy()
|
|
174
|
+
x_features.loc[:, feature_names_scaling] = scaled_features
|
|
145
175
|
|
|
146
176
|
# Get the tremor probability
|
|
147
|
-
df[DataColumns.PRED_TREMOR_PROBA] = clf_package.predict_proba(
|
|
177
|
+
df[DataColumns.PRED_TREMOR_PROBA] = clf_package.predict_proba(x_features)
|
|
148
178
|
|
|
149
179
|
# Make prediction based on pre-defined threshold
|
|
150
180
|
df[DataColumns.PRED_TREMOR_LOGREG] = (
|
|
@@ -152,11 +182,11 @@ def detect_tremor(
|
|
|
152
182
|
).astype(int)
|
|
153
183
|
|
|
154
184
|
# Perform extra checks for rest tremor
|
|
155
|
-
peak_check = (df[
|
|
156
|
-
df[
|
|
185
|
+
peak_check = (df[DataColumns.FREQ_PEAK] >= config.fmin_rest_tremor) & (
|
|
186
|
+
df[DataColumns.FREQ_PEAK] <= config.fmax_rest_tremor
|
|
157
187
|
) # peak within 3-7 Hz
|
|
158
188
|
df[DataColumns.PRED_ARM_AT_REST] = (
|
|
159
|
-
df[
|
|
189
|
+
df[DataColumns.BELOW_TREMOR_POWER] <= config.movement_threshold
|
|
160
190
|
).astype(
|
|
161
191
|
int
|
|
162
192
|
) # arm at rest or in stable posture
|
|
@@ -171,31 +201,39 @@ def detect_tremor(
|
|
|
171
201
|
|
|
172
202
|
def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
|
|
173
203
|
"""
|
|
174
|
-
Quantifies the amount of tremor time and tremor power, aggregated over
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
178
210
|
|
|
179
211
|
Parameters
|
|
180
212
|
----------
|
|
181
213
|
df : pd.DataFrame
|
|
182
|
-
The input DataFrame containing the tremor predictions and computed
|
|
183
|
-
The DataFrame must also contain a datatime column
|
|
214
|
+
The input DataFrame containing the tremor predictions and computed
|
|
215
|
+
tremor power. The DataFrame must also contain a datatime column
|
|
216
|
+
('time_dt').
|
|
184
217
|
|
|
185
218
|
config : TremorConfig
|
|
186
|
-
Configuration object containing the percentile for aggregating tremor
|
|
219
|
+
Configuration object containing the percentile for aggregating tremor
|
|
220
|
+
power.
|
|
187
221
|
|
|
188
222
|
Returns
|
|
189
223
|
-------
|
|
190
224
|
dict
|
|
191
|
-
A dictionary with the aggregated tremor time and tremor power
|
|
192
|
-
|
|
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.
|
|
193
229
|
|
|
194
230
|
Notes
|
|
195
231
|
-----
|
|
196
|
-
- Tremor power is converted to log scale, after adding a constant of
|
|
197
|
-
|
|
198
|
-
|
|
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.
|
|
199
237
|
|
|
200
238
|
"""
|
|
201
239
|
nr_valid_days = (
|
|
@@ -203,7 +241,8 @@ def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
|
|
|
203
241
|
) # number of valid days in the input dataframe
|
|
204
242
|
nr_windows_total = df.shape[0] # number of windows in the input dataframe
|
|
205
243
|
|
|
206
|
-
#
|
|
244
|
+
# Remove windows with detected non-tremor arm movements to control for
|
|
245
|
+
# the amount of arm activities performed
|
|
207
246
|
df_filtered = df.loc[df.pred_arm_at_rest == 1]
|
|
208
247
|
nr_windows_rest = df_filtered.shape[
|
|
209
248
|
0
|
|
@@ -214,20 +253,16 @@ def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
|
|
|
214
253
|
): # if no windows without non-tremor arm movement are detected
|
|
215
254
|
raise Warning("No windows without non-tremor arm movement are detected.")
|
|
216
255
|
|
|
217
|
-
#
|
|
218
|
-
n_windows_tremor = np.sum(df_filtered[
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
) # 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
|
|
222
260
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
) # initialize dictionary to store aggregated tremor power measures
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
n_windows_tremor == 0
|
|
229
|
-
): # 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 = {}
|
|
230
263
|
|
|
264
|
+
# If no tremor is detected, the tremor power measures are set to NaN
|
|
265
|
+
if n_windows_tremor == 0:
|
|
231
266
|
aggregated_tremor_power["median_tremor_power"] = np.nan
|
|
232
267
|
aggregated_tremor_power["mode_binned_tremor_power"] = np.nan
|
|
233
268
|
aggregated_tremor_power["90p_tremor_power"] = np.nan
|
|
@@ -236,7 +271,7 @@ def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
|
|
|
236
271
|
|
|
237
272
|
# calculate aggregated tremor power measures
|
|
238
273
|
tremor_power = df_filtered.loc[
|
|
239
|
-
df_filtered[
|
|
274
|
+
df_filtered[DataColumns.PRED_TREMOR_CHECKED] == 1, DataColumns.TREMOR_POWER
|
|
240
275
|
]
|
|
241
276
|
tremor_power = np.log10(tremor_power + 1) # convert to log scale
|
|
242
277
|
|
|
@@ -268,22 +303,25 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
|
|
|
268
303
|
"""
|
|
269
304
|
Compute spectral domain features from the gyroscope data.
|
|
270
305
|
|
|
271
|
-
This function computes Mel-frequency cepstral coefficients (MFCCs), the
|
|
272
|
-
the tremor power, and the below tremor power
|
|
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.
|
|
273
310
|
|
|
274
311
|
Parameters
|
|
275
312
|
----------
|
|
276
313
|
data : numpy.ndarray
|
|
277
314
|
A 2D numpy array where each row corresponds to a window of gyroscope data.
|
|
278
315
|
config : object
|
|
279
|
-
Configuration object containing settings such as sampling frequency,
|
|
280
|
-
and MFCC parameters.
|
|
316
|
+
Configuration object containing settings such as sampling frequency,
|
|
317
|
+
window type, and MFCC parameters.
|
|
281
318
|
|
|
282
319
|
Returns
|
|
283
320
|
-------
|
|
284
321
|
pd.DataFrame
|
|
285
|
-
The feature dataframe containing the extracted spectral features,
|
|
286
|
-
MFCCs, the frequency of the peak, the tremor power and
|
|
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.
|
|
287
325
|
|
|
288
326
|
"""
|
|
289
327
|
|
|
@@ -321,7 +359,7 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
|
|
|
321
359
|
overlap_n = segment_length_n * overlap_fraction
|
|
322
360
|
window = signal.get_window(window_type, segment_length_n)
|
|
323
361
|
|
|
324
|
-
f, t,
|
|
362
|
+
f, t, stft_result = signal.stft(
|
|
325
363
|
x=data,
|
|
326
364
|
fs=sampling_frequency,
|
|
327
365
|
window=window,
|
|
@@ -331,9 +369,10 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
|
|
|
331
369
|
axis=1,
|
|
332
370
|
)
|
|
333
371
|
|
|
334
|
-
# Compute total power in the PSD and the total spectrogram (summed over
|
|
372
|
+
# Compute total power in the PSD and the total spectrogram (summed over
|
|
373
|
+
# the three axes)
|
|
335
374
|
total_psd = compute_total_power(psd)
|
|
336
|
-
total_spectrogram = np.sum(np.abs(
|
|
375
|
+
total_spectrogram = np.sum(np.abs(stft_result) * sampling_frequency, axis=2)
|
|
337
376
|
|
|
338
377
|
# Compute the MFCC's
|
|
339
378
|
config.mfcc_low_frequency = config.fmin_mfcc
|
|
@@ -355,10 +394,10 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
|
|
|
355
394
|
feature_dict[colname] = mfccs[:, i]
|
|
356
395
|
|
|
357
396
|
# Compute the frequency of the peak, non-tremor power and tremor power
|
|
358
|
-
feature_dict[
|
|
397
|
+
feature_dict[DataColumns.FREQ_PEAK] = extract_frequency_peak(
|
|
359
398
|
freqs, total_psd, config.fmin_peak_search, config.fmax_peak_search
|
|
360
399
|
)
|
|
361
|
-
feature_dict[
|
|
400
|
+
feature_dict[DataColumns.BELOW_TREMOR_POWER] = compute_power_in_bandwidth(
|
|
362
401
|
freqs,
|
|
363
402
|
total_psd,
|
|
364
403
|
config.fmin_below_rest_tremor,
|
|
@@ -367,8 +406,189 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
|
|
|
367
406
|
spectral_resolution=config.spectral_resolution,
|
|
368
407
|
cumulative_sum_method="sum",
|
|
369
408
|
)
|
|
370
|
-
feature_dict[
|
|
409
|
+
feature_dict[DataColumns.TREMOR_POWER] = extract_tremor_power(
|
|
371
410
|
freqs, total_psd, config.fmin_rest_tremor, config.fmax_rest_tremor
|
|
372
411
|
)
|
|
373
412
|
|
|
374
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
|
+
)
|
|
593
|
+
|
|
594
|
+
return df_quantification
|