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.
- paradigma/__init__.py +10 -1
- paradigma/classification.py +38 -21
- paradigma/config.py +187 -123
- paradigma/constants.py +48 -35
- paradigma/feature_extraction.py +345 -255
- paradigma/load.py +476 -0
- paradigma/orchestrator.py +670 -0
- paradigma/pipelines/gait_pipeline.py +685 -246
- paradigma/pipelines/pulse_rate_pipeline.py +456 -155
- paradigma/pipelines/pulse_rate_utils.py +289 -248
- paradigma/pipelines/tremor_pipeline.py +405 -132
- paradigma/prepare_data.py +409 -0
- paradigma/preprocessing.py +500 -163
- paradigma/segmenting.py +180 -140
- paradigma/testing.py +370 -178
- paradigma/util.py +190 -101
- paradigma-1.1.0.dist-info/METADATA +229 -0
- paradigma-1.1.0.dist-info/RECORD +26 -0
- {paradigma-1.0.3.dist-info → paradigma-1.1.0.dist-info}/WHEEL +1 -1
- paradigma-1.1.0.dist-info/entry_points.txt +4 -0
- {paradigma-1.0.3.dist-info → paradigma-1.1.0.dist-info/licenses}/LICENSE +0 -1
- paradigma-1.0.3.dist-info/METADATA +0 -138
- paradigma-1.0.3.dist-info/RECORD +0 -22
|
@@ -1,64 +1,86 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
24
|
-
|
|
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,
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
47
|
-
windowed_data = tabulate_windows(
|
|
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(
|
|
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(
|
|
53
|
-
idx_gyro = extractor.get_slice(config.
|
|
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=[
|
|
60
|
-
|
|
61
|
-
#
|
|
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(
|
|
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
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
86
|
-
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.
|
|
87
119
|
|
|
88
120
|
config : TremorConfig
|
|
89
|
-
Configuration object containing settings for tremor detection,
|
|
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
|
|
93
|
-
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.
|
|
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
|
|
100
|
-
|
|
101
|
-
- `
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
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(
|
|
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] = (
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
156
|
-
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').
|
|
157
217
|
|
|
158
218
|
config : TremorConfig
|
|
159
|
-
Configuration object containing the percentile for aggregating tremor
|
|
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
|
|
165
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
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 =
|
|
175
|
-
|
|
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
|
-
#
|
|
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[
|
|
247
|
+
nr_windows_rest = df_filtered.shape[
|
|
248
|
+
0
|
|
249
|
+
] # number of windows without non-tremor arm movement
|
|
180
250
|
|
|
181
|
-
if
|
|
182
|
-
|
|
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
|
-
#
|
|
185
|
-
n_windows_tremor = np.sum(df_filtered[
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
aggregated_tremor_power[
|
|
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[
|
|
200
|
-
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
229
|
-
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.
|
|
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,
|
|
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,
|
|
243
|
-
MFCCs, the frequency of the peak, the tremor power and
|
|
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 =
|
|
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=
|
|
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,
|
|
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
|
|
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(
|
|
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=
|
|
305
|
-
rounding_method=
|
|
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
|
|
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[
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
594
|
+
return df_quantification
|