paradigma 1.0.0__py3-none-any.whl → 1.0.2__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/config.py +6 -6
- paradigma/constants.py +2 -2
- paradigma/feature_extraction.py +4 -4
- paradigma/pipelines/{heart_rate_pipeline.py → pulse_rate_pipeline.py} +48 -48
- paradigma/pipelines/{heart_rate_utils.py → pulse_rate_utils.py} +26 -26
- paradigma/pipelines/tremor_pipeline.py +3 -3
- paradigma/testing.py +19 -19
- {paradigma-1.0.0.dist-info → paradigma-1.0.2.dist-info}/METADATA +2 -2
- {paradigma-1.0.0.dist-info → paradigma-1.0.2.dist-info}/RECORD +11 -11
- {paradigma-1.0.0.dist-info → paradigma-1.0.2.dist-info}/WHEEL +1 -1
- {paradigma-1.0.0.dist-info → paradigma-1.0.2.dist-info}/LICENSE +0 -0
paradigma/config.py
CHANGED
|
@@ -244,7 +244,7 @@ class TremorConfig(IMUConfig):
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
|
|
247
|
-
class
|
|
247
|
+
class PulseRateConfig(PPGConfig):
|
|
248
248
|
def __init__(self, sensor: str = 'ppg', min_window_length_s: int = 30) -> None:
|
|
249
249
|
super().__init__()
|
|
250
250
|
|
|
@@ -265,14 +265,14 @@ class HeartRateConfig(PPGConfig):
|
|
|
265
265
|
self.freq_bin_resolution = 0.05 # Hz
|
|
266
266
|
|
|
267
267
|
# ---------------------
|
|
268
|
-
#
|
|
268
|
+
# Pulse rate estimation
|
|
269
269
|
# ---------------------
|
|
270
270
|
self.set_tfd_length(min_window_length_s) # Set tfd length to default of 30 seconds
|
|
271
271
|
self.threshold_sqa = 0.5
|
|
272
|
-
self.threshold_sqa_accelerometer = 0.
|
|
272
|
+
self.threshold_sqa_accelerometer = 0.10
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
self.
|
|
274
|
+
pr_est_length = 2 # pulse rate estimation length in seconds
|
|
275
|
+
self.pr_est_samples = pr_est_length * self.sampling_frequency
|
|
276
276
|
|
|
277
277
|
# Time-frequency distribution parameters
|
|
278
278
|
self.kern_type = 'sep'
|
|
@@ -297,7 +297,7 @@ class HeartRateConfig(PPGConfig):
|
|
|
297
297
|
|
|
298
298
|
def set_tfd_length(self, tfd_length: int):
|
|
299
299
|
self.tfd_length = tfd_length
|
|
300
|
-
self.
|
|
300
|
+
self.min_pr_samples = int(round(self.tfd_length * self.sampling_frequency))
|
|
301
301
|
|
|
302
302
|
def set_sensor(self, sensor):
|
|
303
303
|
self.sensor = sensor
|
paradigma/constants.py
CHANGED
|
@@ -58,8 +58,8 @@ class DataColumns():
|
|
|
58
58
|
PRED_SQA_ACC_LABEL: str = "pred_sqa_acc_label"
|
|
59
59
|
PRED_SQA: str = "pred_sqa"
|
|
60
60
|
|
|
61
|
-
# Constants for
|
|
62
|
-
|
|
61
|
+
# Constants for pulse rate
|
|
62
|
+
PULSE_RATE: str = "pulse_rate"
|
|
63
63
|
|
|
64
64
|
@dataclass(frozen=True)
|
|
65
65
|
class DataUnits():
|
paradigma/feature_extraction.py
CHANGED
|
@@ -7,7 +7,7 @@ from scipy.signal import find_peaks, windows
|
|
|
7
7
|
from scipy.stats import kurtosis, skew
|
|
8
8
|
from sklearn.decomposition import PCA
|
|
9
9
|
|
|
10
|
-
from paradigma.config import
|
|
10
|
+
from paradigma.config import PulseRateConfig
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def compute_statistics(data: np.ndarray, statistic: str, abs_stats: bool=False) -> np.ndarray:
|
|
@@ -353,7 +353,7 @@ def extract_frequency_peak(
|
|
|
353
353
|
def compute_relative_power(
|
|
354
354
|
freqs: np.ndarray,
|
|
355
355
|
psd: np.ndarray,
|
|
356
|
-
config:
|
|
356
|
+
config: PulseRateConfig
|
|
357
357
|
) -> list:
|
|
358
358
|
"""
|
|
359
359
|
Calculate relative power within the dominant frequency band in the physiological range (0.75 - 3 Hz).
|
|
@@ -364,11 +364,11 @@ def compute_relative_power(
|
|
|
364
364
|
The frequency bins of the power spectral density.
|
|
365
365
|
psd: np.ndarray
|
|
366
366
|
The power spectral density of the signal.
|
|
367
|
-
config:
|
|
367
|
+
config: PulseRateConfig
|
|
368
368
|
The configuration object containing the parameters for the feature extraction. The following
|
|
369
369
|
attributes are used:
|
|
370
370
|
- freq_band_physio: tuple
|
|
371
|
-
The frequency band for physiological
|
|
371
|
+
The frequency band for physiological pulse rate (default: (0.75, 3)).
|
|
372
372
|
- bandwidth: float
|
|
373
373
|
The bandwidth around the peak frequency to consider for relative power calculation (default: 0.5).
|
|
374
374
|
|
|
@@ -10,14 +10,14 @@ from typing import List
|
|
|
10
10
|
|
|
11
11
|
from paradigma.classification import ClassifierPackage
|
|
12
12
|
from paradigma.constants import DataColumns
|
|
13
|
-
from paradigma.config import
|
|
13
|
+
from paradigma.config import PulseRateConfig
|
|
14
14
|
from paradigma.feature_extraction import compute_statistics, compute_signal_to_noise_ratio, compute_auto_correlation, \
|
|
15
15
|
compute_dominant_frequency, compute_relative_power, compute_spectral_entropy
|
|
16
|
-
from paradigma.pipelines.
|
|
16
|
+
from paradigma.pipelines.pulse_rate_utils import assign_sqa_label, extract_pr_segments, extract_pr_from_segment
|
|
17
17
|
from paradigma.segmenting import tabulate_windows, WindowedDataExtractor
|
|
18
|
-
from paradigma.util import
|
|
18
|
+
from paradigma.util import aggregate_parameter
|
|
19
19
|
|
|
20
|
-
def extract_signal_quality_features(df_ppg: pd.DataFrame, df_acc: pd.DataFrame, ppg_config:
|
|
20
|
+
def extract_signal_quality_features(df_ppg: pd.DataFrame, df_acc: pd.DataFrame, ppg_config: PulseRateConfig, acc_config: PulseRateConfig) -> pd.DataFrame:
|
|
21
21
|
"""
|
|
22
22
|
Extract signal quality features from the PPG signal.
|
|
23
23
|
The features are extracted from the temporal and spectral domain of the PPG signal.
|
|
@@ -30,9 +30,9 @@ def extract_signal_quality_features(df_ppg: pd.DataFrame, df_acc: pd.DataFrame,
|
|
|
30
30
|
The DataFrame containing the PPG signal.
|
|
31
31
|
df_acc : pd.DataFrame
|
|
32
32
|
The DataFrame containing the accelerometer signal.
|
|
33
|
-
ppg_config:
|
|
33
|
+
ppg_config: PulseRateConfig
|
|
34
34
|
The configuration for the signal quality feature extraction of the PPG signal.
|
|
35
|
-
acc_config:
|
|
35
|
+
acc_config: PulseRateConfig
|
|
36
36
|
The configuration for the signal quality feature extraction of the accelerometer signal.
|
|
37
37
|
|
|
38
38
|
Returns
|
|
@@ -94,7 +94,7 @@ def extract_signal_quality_features(df_ppg: pd.DataFrame, df_acc: pd.DataFrame,
|
|
|
94
94
|
return df_features
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def signal_quality_classification(df: pd.DataFrame, config:
|
|
97
|
+
def signal_quality_classification(df: pd.DataFrame, config: PulseRateConfig, full_path_to_classifier_package: str | Path) -> pd.DataFrame:
|
|
98
98
|
"""
|
|
99
99
|
Classify the signal quality of the PPG signal using a logistic regression classifier. A probability close to 1 indicates a high-quality signal, while a probability close to 0 indicates a low-quality signal.
|
|
100
100
|
The classifier is trained on features extracted from the PPG signal. The features are extracted using the extract_signal_quality_features function.
|
|
@@ -105,7 +105,7 @@ def signal_quality_classification(df: pd.DataFrame, config: HeartRateConfig, ful
|
|
|
105
105
|
----------
|
|
106
106
|
df : pd.DataFrame
|
|
107
107
|
The DataFrame containing the PPG features and the accelerometer feature for signal quality classification.
|
|
108
|
-
config :
|
|
108
|
+
config : PulseRateConfig
|
|
109
109
|
The configuration for the signal quality classification.
|
|
110
110
|
full_path_to_classifier_package : str | Path
|
|
111
111
|
The path to the directory containing the classifier.
|
|
@@ -128,9 +128,9 @@ def signal_quality_classification(df: pd.DataFrame, config: HeartRateConfig, ful
|
|
|
128
128
|
return df[[DataColumns.TIME, DataColumns.PRED_SQA_PROBA, DataColumns.PRED_SQA_ACC_LABEL]] # Return only the relevant columns, namely the predicted probabilities for the PPG signal quality and the accelerometer label
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def
|
|
131
|
+
def estimate_pulse_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame, config: PulseRateConfig) -> pd.DataFrame:
|
|
132
132
|
"""
|
|
133
|
-
Estimate the
|
|
133
|
+
Estimate the pulse rate from the PPG signal using the time-frequency domain method.
|
|
134
134
|
|
|
135
135
|
Parameters
|
|
136
136
|
----------
|
|
@@ -138,13 +138,13 @@ def estimate_heart_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame,
|
|
|
138
138
|
The DataFrame containing the signal quality assessment predictions.
|
|
139
139
|
df_ppg_preprocessed : pd.DataFrame
|
|
140
140
|
The DataFrame containing the preprocessed PPG signal.
|
|
141
|
-
config :
|
|
142
|
-
The configuration for the
|
|
141
|
+
config : PulseRateConfig
|
|
142
|
+
The configuration for the pulse rate estimation.
|
|
143
143
|
|
|
144
144
|
Returns
|
|
145
145
|
-------
|
|
146
|
-
|
|
147
|
-
The DataFrame containing the
|
|
146
|
+
df_pr : pd.DataFrame
|
|
147
|
+
The DataFrame containing the pulse rate estimations.
|
|
148
148
|
"""
|
|
149
149
|
|
|
150
150
|
# Extract NumPy arrays for faster operations
|
|
@@ -156,13 +156,13 @@ def estimate_heart_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame,
|
|
|
156
156
|
|
|
157
157
|
# Assign window-level probabilities to individual samples
|
|
158
158
|
sqa_label = assign_sqa_label(ppg_post_prob, config, acc_label) # assigns a signal quality label to every individual data point
|
|
159
|
-
v_start_idx, v_end_idx =
|
|
159
|
+
v_start_idx, v_end_idx = extract_pr_segments(sqa_label, config.min_pr_samples) # extracts pulse rate segments based on the SQA label
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
v_pr_rel = np.array([])
|
|
162
|
+
t_pr_rel = np.array([])
|
|
163
163
|
|
|
164
|
-
edge_add = 2 * config.sampling_frequency # Add 2s on both sides of the segment for
|
|
165
|
-
step_size = config.
|
|
164
|
+
edge_add = 2 * config.sampling_frequency # Add 2s on both sides of the segment for PR estimation
|
|
165
|
+
step_size = config.pr_est_samples # Step size for PR estimation
|
|
166
166
|
|
|
167
167
|
# Estimate the maximum size for preallocation
|
|
168
168
|
valid_segments = (v_start_idx >= edge_add) & (v_end_idx <= len(ppg_preprocessed) - edge_add) # check if the segments are valid, e.g. not too close to the edges (2s)
|
|
@@ -171,55 +171,55 @@ def estimate_heart_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame,
|
|
|
171
171
|
max_size = np.sum((valid_end_idx - valid_start_idx) // step_size) # maximum size for preallocation
|
|
172
172
|
|
|
173
173
|
# Preallocate arrays
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
v_pr_rel = np.empty(max_size, dtype=float)
|
|
175
|
+
t_pr_rel = np.empty(max_size, dtype=float)
|
|
176
176
|
|
|
177
177
|
# Track current position
|
|
178
|
-
|
|
178
|
+
pr_pos = 0
|
|
179
179
|
|
|
180
180
|
for start_idx, end_idx in zip(valid_start_idx, valid_end_idx):
|
|
181
181
|
# Extract extended PPG segment
|
|
182
182
|
extended_ppg_segment = ppg_preprocessed[start_idx - edge_add : end_idx + edge_add, ppg_idx]
|
|
183
183
|
|
|
184
|
-
# Estimate
|
|
185
|
-
|
|
184
|
+
# Estimate pulse rate
|
|
185
|
+
pr_est = extract_pr_from_segment(
|
|
186
186
|
extended_ppg_segment,
|
|
187
187
|
config.tfd_length,
|
|
188
188
|
config.sampling_frequency,
|
|
189
189
|
config.kern_type,
|
|
190
190
|
config.kern_params,
|
|
191
191
|
)
|
|
192
|
-
|
|
193
|
-
end_idx_time =
|
|
192
|
+
n_pr = len(pr_est) # Number of pulse rate estimates
|
|
193
|
+
end_idx_time = n_pr * step_size + start_idx # Calculate end index for time, different from end_idx since it is always a multiple of step_size, while end_idx is not
|
|
194
194
|
|
|
195
|
-
# Extract relative time for
|
|
196
|
-
|
|
195
|
+
# Extract relative time for PR estimates
|
|
196
|
+
pr_time = ppg_preprocessed[start_idx : end_idx_time : step_size, time_idx]
|
|
197
197
|
|
|
198
198
|
# Insert into preallocated arrays
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
v_pr_rel[pr_pos:pr_pos + n_pr] = pr_est
|
|
200
|
+
t_pr_rel[pr_pos:pr_pos + n_pr] = pr_time
|
|
201
|
+
pr_pos += n_pr
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
df_pr = pd.DataFrame({"time": t_pr_rel, "pulse_rate": v_pr_rel})
|
|
204
204
|
|
|
205
|
-
return
|
|
205
|
+
return df_pr
|
|
206
206
|
|
|
207
207
|
|
|
208
|
-
def
|
|
208
|
+
def aggregate_pulse_rate(pr_values: np.ndarray, aggregates: List[str] = ['mode', '99p']) -> dict:
|
|
209
209
|
"""
|
|
210
|
-
Aggregate the
|
|
210
|
+
Aggregate the pulse rate estimates using the specified aggregation methods.
|
|
211
211
|
|
|
212
212
|
Parameters
|
|
213
213
|
----------
|
|
214
|
-
|
|
215
|
-
The array containing the
|
|
214
|
+
pr_values : np.ndarray
|
|
215
|
+
The array containing the pulse rate estimates
|
|
216
216
|
aggregates : List[str]
|
|
217
|
-
The list of aggregation methods to be used for the
|
|
217
|
+
The list of aggregation methods to be used for the pulse rate estimates. The default is ['mode', '99p'].
|
|
218
218
|
|
|
219
219
|
Returns
|
|
220
220
|
-------
|
|
221
221
|
aggregated_results : dict
|
|
222
|
-
The dictionary containing the aggregated results of the
|
|
222
|
+
The dictionary containing the aggregated results of the pulse rate estimates.
|
|
223
223
|
"""
|
|
224
224
|
# Initialize the dictionary for the aggregated results
|
|
225
225
|
aggregated_results = {}
|
|
@@ -227,19 +227,19 @@ def aggregate_heart_rate(hr_values: np.ndarray, aggregates: List[str] = ['mode',
|
|
|
227
227
|
# Initialize the dictionary for the aggregated results with the metadata
|
|
228
228
|
aggregated_results = {
|
|
229
229
|
'metadata': {
|
|
230
|
-
'
|
|
230
|
+
'nr_pr_est': len(pr_values)
|
|
231
231
|
},
|
|
232
|
-
'
|
|
232
|
+
'pr_aggregates': {}
|
|
233
233
|
}
|
|
234
234
|
for aggregate in aggregates:
|
|
235
|
-
aggregated_results['
|
|
235
|
+
aggregated_results['pr_aggregates'][f'{aggregate}_{DataColumns.PULSE_RATE}'] = aggregate_parameter(pr_values, aggregate)
|
|
236
236
|
|
|
237
237
|
return aggregated_results
|
|
238
238
|
|
|
239
239
|
|
|
240
240
|
def extract_temporal_domain_features(
|
|
241
241
|
ppg_windowed: np.ndarray,
|
|
242
|
-
config:
|
|
242
|
+
config: PulseRateConfig,
|
|
243
243
|
quality_stats: List[str] = ['mean', 'std']
|
|
244
244
|
) -> pd.DataFrame:
|
|
245
245
|
"""
|
|
@@ -250,7 +250,7 @@ def extract_temporal_domain_features(
|
|
|
250
250
|
ppg_windowed: np.ndarray
|
|
251
251
|
The dataframe containing the windowed accelerometer signal
|
|
252
252
|
|
|
253
|
-
config:
|
|
253
|
+
config: PulseRateConfig
|
|
254
254
|
The configuration object containing the parameters for the feature extraction
|
|
255
255
|
|
|
256
256
|
quality_stats: list, optional
|
|
@@ -273,7 +273,7 @@ def extract_temporal_domain_features(
|
|
|
273
273
|
|
|
274
274
|
def extract_spectral_domain_features(
|
|
275
275
|
ppg_windowed: np.ndarray,
|
|
276
|
-
config:
|
|
276
|
+
config: PulseRateConfig,
|
|
277
277
|
) -> pd.DataFrame:
|
|
278
278
|
"""
|
|
279
279
|
Calculate the spectral features (dominant frequency, relative power, and spectral entropy)
|
|
@@ -285,7 +285,7 @@ def extract_spectral_domain_features(
|
|
|
285
285
|
ppg_windowed: np.ndarray
|
|
286
286
|
The dataframe containing the windowed ppg signal
|
|
287
287
|
|
|
288
|
-
config:
|
|
288
|
+
config: PulseRateConfig
|
|
289
289
|
The configuration object containing the parameters for the feature extraction
|
|
290
290
|
|
|
291
291
|
Returns
|
|
@@ -371,7 +371,7 @@ def extract_acc_power_feature(
|
|
|
371
371
|
def extract_accelerometer_feature(
|
|
372
372
|
acc_windowed: np.ndarray,
|
|
373
373
|
ppg_windowed: np.ndarray,
|
|
374
|
-
config:
|
|
374
|
+
config: PulseRateConfig
|
|
375
375
|
) -> pd.DataFrame:
|
|
376
376
|
"""
|
|
377
377
|
Extract accelerometer features from the accelerometer signal in the PPG frequency range.
|
|
@@ -384,7 +384,7 @@ def extract_accelerometer_feature(
|
|
|
384
384
|
ppg_windowed: np.ndarray
|
|
385
385
|
The dataframe containing the corresponding windowed ppg signal
|
|
386
386
|
|
|
387
|
-
config:
|
|
387
|
+
config: PulseRateConfig
|
|
388
388
|
The configuration object containing the parameters for the feature extraction
|
|
389
389
|
|
|
390
390
|
Returns
|
|
@@ -2,12 +2,12 @@ import numpy as np
|
|
|
2
2
|
from scipy import signal
|
|
3
3
|
from typing import Tuple
|
|
4
4
|
|
|
5
|
-
from paradigma.config import
|
|
5
|
+
from paradigma.config import PulseRateConfig
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def assign_sqa_label(
|
|
9
9
|
ppg_prob: np.ndarray,
|
|
10
|
-
config:
|
|
10
|
+
config: PulseRateConfig,
|
|
11
11
|
acc_label=None
|
|
12
12
|
) -> np.ndarray:
|
|
13
13
|
"""
|
|
@@ -17,7 +17,7 @@ def assign_sqa_label(
|
|
|
17
17
|
----------
|
|
18
18
|
ppg_prob : np.ndarray
|
|
19
19
|
The probabilities for PPG.
|
|
20
|
-
config :
|
|
20
|
+
config : PulseRateConfig
|
|
21
21
|
The configuration parameters.
|
|
22
22
|
acc_label : np.ndarray, optional
|
|
23
23
|
The labels for the accelerometer.
|
|
@@ -61,23 +61,23 @@ def assign_sqa_label(
|
|
|
61
61
|
return sqa_label
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def
|
|
64
|
+
def extract_pr_segments(sqa_label: np.ndarray, min_pr_samples: int) -> Tuple[np.ndarray, np.ndarray]:
|
|
65
65
|
"""
|
|
66
|
-
Extracts
|
|
66
|
+
Extracts pulse rate segments based on the SQA label.
|
|
67
67
|
|
|
68
68
|
Parameters
|
|
69
69
|
----------
|
|
70
70
|
sqa_label : np.ndarray
|
|
71
71
|
The signal quality assessment label.
|
|
72
|
-
|
|
73
|
-
The minimum number of samples required for a
|
|
72
|
+
min_pr_samples : int
|
|
73
|
+
The minimum number of samples required for a pulse rate segment.
|
|
74
74
|
|
|
75
75
|
Returns
|
|
76
76
|
-------
|
|
77
77
|
Tuple[v_start_idx_long, v_end_idx_long]
|
|
78
|
-
The start and end indices of the
|
|
78
|
+
The start and end indices of the pulse rate segments.
|
|
79
79
|
"""
|
|
80
|
-
# Find the start and end indices of the
|
|
80
|
+
# Find the start and end indices of the pulse rate segments
|
|
81
81
|
v_start_idx = np.where(np.diff(sqa_label.astype(int)) == 1)[0] + 1
|
|
82
82
|
v_end_idx = np.where(np.diff(sqa_label.astype(int)) == -1)[0] + 1
|
|
83
83
|
|
|
@@ -88,13 +88,13 @@ def extract_hr_segments(sqa_label: np.ndarray, min_hr_samples: int) -> Tuple[np.
|
|
|
88
88
|
v_end_idx = np.append(v_end_idx, len(sqa_label))
|
|
89
89
|
|
|
90
90
|
# Check if the segments are long enough
|
|
91
|
-
v_start_idx_long = v_start_idx[(v_end_idx - v_start_idx) >=
|
|
92
|
-
v_end_idx_long = v_end_idx[(v_end_idx - v_start_idx) >=
|
|
91
|
+
v_start_idx_long = v_start_idx[(v_end_idx - v_start_idx) >= min_pr_samples]
|
|
92
|
+
v_end_idx_long = v_end_idx[(v_end_idx - v_start_idx) >= min_pr_samples]
|
|
93
93
|
|
|
94
94
|
return v_start_idx_long, v_end_idx_long
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def
|
|
97
|
+
def extract_pr_from_segment(
|
|
98
98
|
ppg: np.ndarray,
|
|
99
99
|
tfd_length: int,
|
|
100
100
|
fs: int,
|
|
@@ -102,7 +102,7 @@ def extract_hr_from_segment(
|
|
|
102
102
|
kern_params: dict
|
|
103
103
|
) -> np.ndarray:
|
|
104
104
|
"""
|
|
105
|
-
Extracts
|
|
105
|
+
Extracts pulse rate from the time-frequency distribution of the PPG signal.
|
|
106
106
|
|
|
107
107
|
Parameters
|
|
108
108
|
----------
|
|
@@ -121,7 +121,7 @@ def extract_hr_from_segment(
|
|
|
121
121
|
Returns
|
|
122
122
|
-------
|
|
123
123
|
np.ndarray
|
|
124
|
-
The estimated
|
|
124
|
+
The estimated pulse rate.
|
|
125
125
|
"""
|
|
126
126
|
|
|
127
127
|
# Constants to handle boundary effects
|
|
@@ -145,23 +145,23 @@ def extract_hr_from_segment(
|
|
|
145
145
|
end_idx = len(ppg)
|
|
146
146
|
ppg_segments.append(ppg[start_idx:end_idx])
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
pr_est_from_ppg = np.array([])
|
|
149
149
|
for segment in ppg_segments:
|
|
150
150
|
# Calculate the time-frequency distribution
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
pr_tfd = extract_pr_with_tfd(segment, fs, kern_type, kern_params)
|
|
152
|
+
pr_est_from_ppg = np.concatenate((pr_est_from_ppg, pr_tfd))
|
|
153
153
|
|
|
154
|
-
return
|
|
154
|
+
return pr_est_from_ppg
|
|
155
155
|
|
|
156
156
|
|
|
157
|
-
def
|
|
157
|
+
def extract_pr_with_tfd(
|
|
158
158
|
ppg: np.ndarray,
|
|
159
159
|
fs: int,
|
|
160
160
|
kern_type: str,
|
|
161
161
|
kern_params: dict
|
|
162
162
|
) -> np.ndarray:
|
|
163
163
|
"""
|
|
164
|
-
Estimate
|
|
164
|
+
Estimate pulse rate (PR) from a PPG segment using a TFD method with optional
|
|
165
165
|
moving average filtering.
|
|
166
166
|
|
|
167
167
|
Parameters
|
|
@@ -177,8 +177,8 @@ def extract_hr_with_tfd(
|
|
|
177
177
|
|
|
178
178
|
Returns
|
|
179
179
|
-------
|
|
180
|
-
|
|
181
|
-
Estimated
|
|
180
|
+
pr_smooth_tfd : np.ndarray
|
|
181
|
+
Estimated pr values (in beats per minute) for each 2-second segment of the PPG signal.
|
|
182
182
|
"""
|
|
183
183
|
# Generate the TFD matrix using the specified kernel
|
|
184
184
|
tfd_obj = TimeFreqDistr()
|
|
@@ -189,16 +189,16 @@ def extract_hr_with_tfd(
|
|
|
189
189
|
time_axis = np.arange(num_time_samples) / fs
|
|
190
190
|
freq_axis = np.linspace(0, 0.5, num_freq_bins) * fs
|
|
191
191
|
|
|
192
|
-
# Estimate
|
|
192
|
+
# Estimate pulse rate by identifying the max frequency in the TFD
|
|
193
193
|
max_freq_indices = np.argmax(tfd, axis=0)
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
pr_smooth_tfd = np.array([])
|
|
196
196
|
for i in range(2, int(len(ppg) / fs) - 4 + 1, 2): # Skip the first and last 2 seconds, add 1 to include the last segment
|
|
197
197
|
relevant_indices = (time_axis >= i) & (time_axis < i + 2)
|
|
198
198
|
avg_frequency = np.mean(freq_axis[max_freq_indices[relevant_indices]])
|
|
199
|
-
|
|
199
|
+
pr_smooth_tfd = np.concatenate((pr_smooth_tfd, [60 * avg_frequency])) # Convert frequency to BPM
|
|
200
200
|
|
|
201
|
-
return
|
|
201
|
+
return pr_smooth_tfd
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
class TimeFreqDistr:
|
|
@@ -143,7 +143,6 @@ def detect_tremor(df: pd.DataFrame, config: TremorConfig, full_path_to_classifie
|
|
|
143
143
|
|
|
144
144
|
return df
|
|
145
145
|
|
|
146
|
-
|
|
147
146
|
def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
|
|
148
147
|
"""
|
|
149
148
|
Quantifies the amount of tremor time and tremor power, aggregated over all windows in the input dataframe.
|
|
@@ -154,8 +153,8 @@ def aggregate_tremor(df: pd.DataFrame, config: TremorConfig):
|
|
|
154
153
|
Parameters
|
|
155
154
|
----------
|
|
156
155
|
df : pd.DataFrame
|
|
157
|
-
The input DataFrame containing
|
|
158
|
-
|
|
156
|
+
The input DataFrame containing the tremor predictions and computed tremor power.
|
|
157
|
+
The DataFrame must also contain a datatime column ('time_dt').
|
|
159
158
|
|
|
160
159
|
config : TremorConfig
|
|
161
160
|
Configuration object containing the percentile for aggregating tremor power.
|
|
@@ -251,6 +250,7 @@ def extract_spectral_domain_features(data: np.ndarray, config) -> pd.DataFrame:
|
|
|
251
250
|
pd.DataFrame
|
|
252
251
|
The feature dataframe containing the extracted spectral features, including
|
|
253
252
|
MFCCs, the frequency of the peak, the tremor power and below tremor power for each window.
|
|
253
|
+
|
|
254
254
|
"""
|
|
255
255
|
|
|
256
256
|
# Initialize a dictionary to hold the results
|
paradigma/testing.py
CHANGED
|
@@ -7,14 +7,14 @@ import tsdf
|
|
|
7
7
|
from typing import List
|
|
8
8
|
|
|
9
9
|
from paradigma.classification import ClassifierPackage
|
|
10
|
-
from paradigma.config import IMUConfig, PPGConfig, GaitConfig, TremorConfig,
|
|
10
|
+
from paradigma.config import IMUConfig, PPGConfig, GaitConfig, TremorConfig, PulseRateConfig
|
|
11
11
|
from paradigma.constants import DataColumns, TimeUnit
|
|
12
12
|
from paradigma.pipelines.gait_pipeline import extract_gait_features, detect_gait, \
|
|
13
13
|
extract_arm_activity_features, filter_gait
|
|
14
14
|
from paradigma.pipelines.tremor_pipeline import extract_tremor_features, detect_tremor, \
|
|
15
15
|
aggregate_tremor
|
|
16
|
-
from paradigma.pipelines.
|
|
17
|
-
|
|
16
|
+
from paradigma.pipelines.pulse_rate_pipeline import extract_signal_quality_features, signal_quality_classification, \
|
|
17
|
+
aggregate_pulse_rate
|
|
18
18
|
from paradigma.preprocessing import preprocess_imu_data, preprocess_ppg_data
|
|
19
19
|
from paradigma.util import read_metadata, write_df_data, get_end_iso8601, merge_predictions_with_timestamps
|
|
20
20
|
|
|
@@ -353,7 +353,7 @@ def aggregate_tremor_io(path_to_feature_input: str | Path, path_to_prediction_in
|
|
|
353
353
|
json.dump(d_aggregates, json_file, indent=4)
|
|
354
354
|
|
|
355
355
|
|
|
356
|
-
def extract_signal_quality_features_io(input_path: str | Path, output_path: str | Path, ppg_config:
|
|
356
|
+
def extract_signal_quality_features_io(input_path: str | Path, output_path: str | Path, ppg_config: PulseRateConfig, acc_config: PulseRateConfig) -> pd.DataFrame:
|
|
357
357
|
"""
|
|
358
358
|
Extract signal quality features from the PPG signal and save them to a file.
|
|
359
359
|
|
|
@@ -363,9 +363,9 @@ def extract_signal_quality_features_io(input_path: str | Path, output_path: str
|
|
|
363
363
|
The path to the directory containing the preprocessed PPG and accelerometer data.
|
|
364
364
|
output_path : str | Path
|
|
365
365
|
The path to the directory where the extracted features will be saved.
|
|
366
|
-
ppg_config:
|
|
366
|
+
ppg_config: PulseRateConfig
|
|
367
367
|
The configuration for the signal quality feature extraction of the ppg signal.
|
|
368
|
-
acc_config:
|
|
368
|
+
acc_config: PulseRateConfig
|
|
369
369
|
The configuration for the signal quality feature extraction of the accelerometer signal.
|
|
370
370
|
|
|
371
371
|
Returns
|
|
@@ -390,7 +390,7 @@ def extract_signal_quality_features_io(input_path: str | Path, output_path: str
|
|
|
390
390
|
return df_windowed
|
|
391
391
|
|
|
392
392
|
|
|
393
|
-
def signal_quality_classification_io(input_path: str | Path, output_path: str | Path, path_to_classifier_input: str | Path, config:
|
|
393
|
+
def signal_quality_classification_io(input_path: str | Path, output_path: str | Path, path_to_classifier_input: str | Path, config: PulseRateConfig) -> None:
|
|
394
394
|
|
|
395
395
|
# Load the data
|
|
396
396
|
metadata_time, metadata_values = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename)
|
|
@@ -399,32 +399,32 @@ def signal_quality_classification_io(input_path: str | Path, output_path: str |
|
|
|
399
399
|
df_sqa = signal_quality_classification(df_windowed, config, path_to_classifier_input)
|
|
400
400
|
|
|
401
401
|
|
|
402
|
-
def
|
|
402
|
+
def aggregate_pulse_rate_io(
|
|
403
403
|
full_path_to_input: str | Path,
|
|
404
404
|
full_path_to_output: str | Path,
|
|
405
405
|
aggregates: List[str] = ['mode', '99p']
|
|
406
406
|
) -> None:
|
|
407
407
|
"""
|
|
408
|
-
Extract
|
|
408
|
+
Extract pulse rate from the PPG signal and save the aggregated pulse rate estimates to a file.
|
|
409
409
|
|
|
410
410
|
Parameters
|
|
411
411
|
----------
|
|
412
412
|
input_path : str | Path
|
|
413
|
-
The path to the directory containing the
|
|
413
|
+
The path to the directory containing the pulse rate estimates.
|
|
414
414
|
output_path : str | Path
|
|
415
|
-
The path to the directory where the aggregated
|
|
415
|
+
The path to the directory where the aggregated pulse rate estimates will be saved.
|
|
416
416
|
aggregates : List[str]
|
|
417
|
-
The list of aggregation methods to be used for the
|
|
417
|
+
The list of aggregation methods to be used for the pulse rate estimates. The default is ['mode', '99p'].
|
|
418
418
|
"""
|
|
419
419
|
|
|
420
|
-
# Load the
|
|
420
|
+
# Load the pulse rate estimates
|
|
421
421
|
with open(full_path_to_input, 'r') as f:
|
|
422
|
-
|
|
422
|
+
df_pr = json.load(f)
|
|
423
423
|
|
|
424
|
-
# Aggregate the
|
|
425
|
-
|
|
426
|
-
|
|
424
|
+
# Aggregate the pulse rate estimates
|
|
425
|
+
pr_values = df_pr['pulse_rate'].values
|
|
426
|
+
df_pr_aggregates = aggregate_pulse_rate(pr_values, aggregates)
|
|
427
427
|
|
|
428
|
-
# Save the aggregated
|
|
428
|
+
# Save the aggregated pulse rate estimates
|
|
429
429
|
with open(full_path_to_output, 'w') as json_file:
|
|
430
|
-
json.dump(
|
|
430
|
+
json.dump(df_pr_aggregates, json_file, indent=4)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: paradigma
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: ParaDigMa - A toolbox for deriving Parkinson's disease Digital Markers from real-life wrist sensor data
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Erik Post
|
|
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|:----:|----|
|
|
27
27
|
| **Packages and Releases** | [](https://github.com/biomarkersparkinson/paradigma/releases/latest) [](https://pypi.python.org/pypi/paradigma/) [](https://research-software-directory.org/software/paradigma) |
|
|
28
28
|
| **DOI** | [](https://doi.org/10.5281/zenodo.13838392) |
|
|
29
|
-
| **Build Status** | [](https://www.python.org/downloads/) [](https://github.com/biomarkersParkinson/paradigma/actions/workflows/build-and-test.yml) [](https://github.com/biomarkersParkinson/paradigma/actions/workflows/pages/pages-build-deployment) |
|
|
30
30
|
| **License** | [](https://github.com/biomarkersparkinson/paradigma/blob/main/LICENSE) |
|
|
31
31
|
<!-- | **Fairness** | [](https://fair-software.eu) [](https://www.bestpractices.dev/projects/8083) | -->
|
|
32
32
|
|
|
@@ -4,19 +4,19 @@ paradigma/assets/gait_filtering_clf_package.pkl,sha256=lAaLyhmXdV4X_drmYt0EM6wGw
|
|
|
4
4
|
paradigma/assets/ppg_quality_clf_package.pkl,sha256=vUcM4v8gZwWAmDVK7E4UcHhVnhlEg27RSB71oPGloSc,1292
|
|
5
5
|
paradigma/assets/tremor_detection_clf_package.pkl,sha256=S-KsK1EcUBJX6oGGBo8GqU0AhNZThA6Qe-cs0QPcWw4,1475
|
|
6
6
|
paradigma/classification.py,sha256=sBJSePvwHZNPUQuLdx-pncfnDzMq-1naomsCxSJneWY,2921
|
|
7
|
-
paradigma/config.py,sha256=
|
|
8
|
-
paradigma/constants.py,sha256=
|
|
9
|
-
paradigma/feature_extraction.py,sha256=
|
|
7
|
+
paradigma/config.py,sha256=hGmWpK1sjwjlmCn43bBa6DEWBTDZjRXlGANIp6X42mY,11206
|
|
8
|
+
paradigma/constants.py,sha256=gR--OzxaZqS5nJnYlWLqnJ9xN05_GMNtd6ec3upsfms,3543
|
|
9
|
+
paradigma/feature_extraction.py,sha256=zgu_fW1zpPvHxpgsPVpJILUiyWH44b9n1bGG7lV2HwE,35323
|
|
10
10
|
paradigma/pipelines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
paradigma/pipelines/gait_pipeline.py,sha256=ZhAc2RZbBX52SJ8hvSRjb5THM47WCfY50iEdImlszJM,26231
|
|
12
|
-
paradigma/pipelines/
|
|
13
|
-
paradigma/pipelines/
|
|
14
|
-
paradigma/pipelines/tremor_pipeline.py,sha256=
|
|
12
|
+
paradigma/pipelines/pulse_rate_pipeline.py,sha256=aBDopwWvfabLCQM6De9PHNKKzL03xD_29jWcsElnjCw,17711
|
|
13
|
+
paradigma/pipelines/pulse_rate_utils.py,sha256=rlXze04meLFlyPaxMBYhvz3_vu3SM77RF-7mLPegTm0,26772
|
|
14
|
+
paradigma/pipelines/tremor_pipeline.py,sha256=qsKEV3QFPQ4bsTGdEX0nXHVMjVlBUpcEHbWEDcGsmVw,14758
|
|
15
15
|
paradigma/preprocessing.py,sha256=OcrwiyNjZpw41IKCf9QRY75A-532kU4gSSSXjqWuTeE,14556
|
|
16
16
|
paradigma/segmenting.py,sha256=hgT4dtg23eyvjUraEXCzX8u0kSRx4vArjQgF10r61P8,13909
|
|
17
|
-
paradigma/testing.py,sha256=
|
|
17
|
+
paradigma/testing.py,sha256=zWPBj7Q1Td6rgeMGoAWi6rIVLB8M6_FNUxlZSbpWqEM,18547
|
|
18
18
|
paradigma/util.py,sha256=E1keTX7vMDowSUG1AGx3juUMIXD7znaGwuqWDfQTpXo,16424
|
|
19
|
-
paradigma-1.0.
|
|
20
|
-
paradigma-1.0.
|
|
21
|
-
paradigma-1.0.
|
|
22
|
-
paradigma-1.0.
|
|
19
|
+
paradigma-1.0.2.dist-info/LICENSE,sha256=Lda8kIVC2kbmlSeYaUWwUwV75Q-q31idYvo18HUTfiw,9807
|
|
20
|
+
paradigma-1.0.2.dist-info/METADATA,sha256=XkUFT1LRgM2taKEqno1cZQl3c-KuCxJJd4kIUOckfE8,11654
|
|
21
|
+
paradigma-1.0.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
22
|
+
paradigma-1.0.2.dist-info/RECORD,,
|
|
File without changes
|