paradigma 0.3.2__py3-none-any.whl → 0.4.1__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/assets/gait_detection_clf_package.pkl +0 -0
- paradigma/assets/gait_filtering_clf_package.pkl +0 -0
- paradigma/assets/ppg_quality_clf_package.pkl +0 -0
- paradigma/assets/tremor_detection_clf_package.pkl +0 -0
- paradigma/classification.py +115 -0
- paradigma/config.py +314 -0
- paradigma/constants.py +48 -7
- paradigma/feature_extraction.py +811 -547
- paradigma/pipelines/__init__.py +0 -0
- paradigma/pipelines/gait_pipeline.py +727 -0
- paradigma/pipelines/heart_rate_pipeline.py +426 -0
- paradigma/pipelines/heart_rate_utils.py +780 -0
- paradigma/pipelines/tremor_pipeline.py +299 -0
- paradigma/preprocessing.py +363 -0
- paradigma/segmenting.py +396 -0
- paradigma/testing.py +416 -0
- paradigma/util.py +393 -16
- paradigma-0.4.1.dist-info/METADATA +138 -0
- paradigma-0.4.1.dist-info/RECORD +22 -0
- {paradigma-0.3.2.dist-info → paradigma-0.4.1.dist-info}/WHEEL +1 -1
- paradigma/gait_analysis.py +0 -415
- paradigma/gait_analysis_config.py +0 -266
- paradigma/heart_rate_analysis.py +0 -127
- paradigma/heart_rate_analysis_config.py +0 -9
- paradigma/heart_rate_util.py +0 -173
- paradigma/imu_preprocessing.py +0 -232
- paradigma/ppg/classifier/LR_PPG_quality.pkl +0 -0
- paradigma/ppg/classifier/LR_model.mat +0 -0
- paradigma/ppg/feat_extraction/acc_feature.m +0 -20
- paradigma/ppg/feat_extraction/peakdet.m +0 -64
- paradigma/ppg/feat_extraction/ppg_features.m +0 -53
- paradigma/ppg/glob_functions/extract_hr_segments.m +0 -37
- paradigma/ppg/glob_functions/extract_overlapping_segments.m +0 -23
- paradigma/ppg/glob_functions/jsonlab/AUTHORS.txt +0 -41
- paradigma/ppg/glob_functions/jsonlab/ChangeLog.txt +0 -74
- paradigma/ppg/glob_functions/jsonlab/LICENSE_BSD.txt +0 -25
- paradigma/ppg/glob_functions/jsonlab/LICENSE_GPLv3.txt +0 -699
- paradigma/ppg/glob_functions/jsonlab/README.txt +0 -394
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/entries +0 -368
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_jsonlab_basic.m.svn-base +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_ubjson_basic.m.svn-base +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example1.json.svn-base +0 -23
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example2.json.svn-base +0 -22
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example3.json.svn-base +0 -11
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example4.json.svn-base +0 -34
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_basictest.matlab.svn-base +0 -662
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.m.svn-base +0 -27
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.matlab.svn-base +0 -144
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_speedtest.m.svn-base +0 -21
- paradigma/ppg/glob_functions/jsonlab/examples/demo_jsonlab_basic.m +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/demo_ubjson_basic.m +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/example1.json +0 -23
- paradigma/ppg/glob_functions/jsonlab/examples/example2.json +0 -22
- paradigma/ppg/glob_functions/jsonlab/examples/example3.json +0 -11
- paradigma/ppg/glob_functions/jsonlab/examples/example4.json +0 -34
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_basictest.matlab +0 -662
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.m +0 -27
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.matlab +0 -144
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_speedtest.m +0 -21
- paradigma/ppg/glob_functions/jsonlab/jsonopt.m +0 -32
- paradigma/ppg/glob_functions/jsonlab/loadjson.m +0 -566
- paradigma/ppg/glob_functions/jsonlab/loadubjson.m +0 -528
- paradigma/ppg/glob_functions/jsonlab/mergestruct.m +0 -33
- paradigma/ppg/glob_functions/jsonlab/savejson.m +0 -475
- paradigma/ppg/glob_functions/jsonlab/saveubjson.m +0 -504
- paradigma/ppg/glob_functions/jsonlab/varargin2struct.m +0 -40
- paradigma/ppg/glob_functions/sample_prob_final.m +0 -49
- paradigma/ppg/glob_functions/synchronization.m +0 -76
- paradigma/ppg/glob_functions/tsdf_scan_meta.m +0 -22
- paradigma/ppg/hr_functions/Long_TFD_JOT.m +0 -37
- paradigma/ppg/hr_functions/PPG_TFD_HR.m +0 -59
- paradigma/ppg/hr_functions/TFD toolbox JOT/.gitignore +0 -4
- paradigma/ppg/hr_functions/TFD toolbox JOT/CHANGELOG.md +0 -23
- paradigma/ppg/hr_functions/TFD toolbox JOT/LICENCE.md +0 -27
- paradigma/ppg/hr_functions/TFD toolbox JOT/README.md +0 -251
- paradigma/ppg/hr_functions/TFD toolbox JOT/README.pdf +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_kern.m +0 -142
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_lag_kern.m +0 -314
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_lag_kern.m +0 -123
- paradigma/ppg/hr_functions/TFD toolbox JOT/dec_tfd.m +0 -154
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_di_gdtfd.m +0 -194
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_li_gdtfd.m +0 -200
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_nonsep_gdtfd.m +0 -229
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_sep_gdtfd.m +0 -241
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/di_gdtfd.m +0 -157
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/li_gdtfd.m +0 -190
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/nonsep_gdtfd.m +0 -196
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/sep_gdtfd.m +0 -199
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_tfd.m +0 -144
- paradigma/ppg/hr_functions/TFD toolbox JOT/load_curdir.m +0 -13
- paradigma/ppg/hr_functions/TFD toolbox JOT/pics/decimated_TFDs_examples.png +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/pics/full_TFDs_examples.png +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/check_dec_params_seq.m +0 -79
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispEE.m +0 -9
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispVars.m +0 -26
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/disp_bytes.m +0 -25
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_full.m +0 -40
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_half.m +0 -34
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/gen_LFM.m +0 -29
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_analytic_signal.m +0 -76
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_window.m +0 -176
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/isreal_fn.m +0 -11
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/padWin.m +0 -97
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/vtfd.m +0 -149
- paradigma/ppg/preprocessing/preprocessing_imu.m +0 -15
- paradigma/ppg/preprocessing/preprocessing_ppg.m +0 -13
- paradigma/ppg_preprocessing.py +0 -313
- paradigma/preprocessing_config.py +0 -69
- paradigma/quantification.py +0 -58
- paradigma/tremor/TremorFeaturesAndClassification.m +0 -345
- paradigma/tremor/feat_extraction/DerivativesExtract.m +0 -22
- paradigma/tremor/feat_extraction/ExtractBandSignalsRMS.m +0 -72
- paradigma/tremor/feat_extraction/MFCCExtract.m +0 -100
- paradigma/tremor/feat_extraction/PSDBandPower.m +0 -52
- paradigma/tremor/feat_extraction/PSDEst.m +0 -63
- paradigma/tremor/feat_extraction/PSDExtrAxis.m +0 -88
- paradigma/tremor/feat_extraction/PSDExtrOpt.m +0 -95
- paradigma/tremor/preprocessing/InterpData.m +0 -32
- paradigma/tremor/weekly_aggregates/WeeklyAggregates.m +0 -295
- paradigma/windowing.py +0 -219
- paradigma-0.3.2.dist-info/METADATA +0 -79
- paradigma-0.3.2.dist-info/RECORD +0 -108
- {paradigma-0.3.2.dist-info → paradigma-0.4.1.dist-info}/LICENSE +0 -0
paradigma/util.py
CHANGED
|
@@ -1,50 +1,427 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import numpy as np
|
|
3
4
|
import pandas as pd
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from datetime import timedelta
|
|
5
7
|
from dateutil import parser
|
|
6
|
-
from typing import Tuple
|
|
8
|
+
from typing import List, Tuple
|
|
7
9
|
|
|
8
10
|
import tsdf
|
|
9
11
|
from tsdf import TSDFMetadata
|
|
10
12
|
|
|
13
|
+
from paradigma.constants import DataColumns, TimeUnit
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
def parse_iso8601_to_datetime(date_str):
|
|
12
17
|
return parser.parse(date_str)
|
|
13
18
|
|
|
19
|
+
|
|
14
20
|
def format_datetime_to_iso8601(datetime):
|
|
15
|
-
return datetime.strftime(
|
|
21
|
+
return datetime.strftime("%Y-%m-%dT%H:%M:%S") + "Z"
|
|
22
|
+
|
|
16
23
|
|
|
17
24
|
def get_end_iso8601(start_iso8601, window_length_seconds):
|
|
18
25
|
start_date = parser.parse(start_iso8601)
|
|
19
26
|
end_date = start_date + timedelta(seconds=window_length_seconds)
|
|
20
27
|
return format_datetime_to_iso8601(end_date)
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
|
|
30
|
+
def write_np_data(
|
|
31
|
+
metadata_time: TSDFMetadata,
|
|
32
|
+
np_array_time: np.ndarray,
|
|
33
|
+
metadata_values: TSDFMetadata,
|
|
34
|
+
np_array_values: np.ndarray,
|
|
35
|
+
output_path: str,
|
|
36
|
+
output_filename: str,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Write the numpy arrays to binary files and store the metadata.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
metadata_time : TSDFMetadata
|
|
44
|
+
Metadata for the time column.
|
|
45
|
+
np_array_time : np.ndarray
|
|
46
|
+
The numpy array for the time column.
|
|
47
|
+
metadata_values : TSDFMetadata
|
|
48
|
+
Metadata for the samples columns.
|
|
49
|
+
np_array_values : np.ndarray
|
|
50
|
+
The numpy array for the samples columns.
|
|
51
|
+
output_path : str
|
|
52
|
+
The path where the files will be stored.
|
|
53
|
+
output_filename : str
|
|
54
|
+
The filename for the metadata.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if not os.path.exists(output_path):
|
|
59
|
+
os.makedirs(output_path)
|
|
60
|
+
|
|
61
|
+
# TODO: improve the way the metadata is stored at a different location
|
|
62
|
+
metadata_time.file_dir_path = output_path
|
|
63
|
+
metadata_values.file_dir_path = output_path
|
|
64
|
+
|
|
65
|
+
# store binaries and metadata
|
|
66
|
+
time_tsdf = tsdf.write_binary_file(file_dir=output_path, file_name=metadata_time.file_name, data=np_array_time, metadata=metadata_time.get_plain_tsdf_dict_copy())
|
|
67
|
+
|
|
68
|
+
samples_tsdf = tsdf.write_binary_file(file_dir=output_path, file_name=metadata_values.file_name, data=np_array_values, metadata=metadata_values.get_plain_tsdf_dict_copy())
|
|
69
|
+
|
|
70
|
+
tsdf.write_metadata([time_tsdf, samples_tsdf], output_filename)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def write_df_data(
|
|
74
|
+
metadata_time: TSDFMetadata,
|
|
75
|
+
metadata_values: TSDFMetadata,
|
|
76
|
+
output_path: str,
|
|
77
|
+
output_filename: str,
|
|
78
|
+
df: pd.DataFrame,
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
Write the Pandas DataFrame to binary files and store the metadata.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
metadata_time : TSDFMetadata
|
|
86
|
+
Metadata for the time column.
|
|
87
|
+
metadata_values : TSDFMetadata
|
|
88
|
+
Metadata for the samples columns.
|
|
89
|
+
output_path : str
|
|
90
|
+
The path where the files will be stored.
|
|
91
|
+
output_filename : str
|
|
92
|
+
The filename for the metadata.
|
|
93
|
+
df : pd.DataFrame
|
|
94
|
+
The DataFrame to be stored.
|
|
95
|
+
|
|
96
|
+
"""
|
|
24
97
|
if not os.path.exists(output_path):
|
|
25
98
|
os.makedirs(output_path)
|
|
26
99
|
|
|
27
100
|
# Make sure the iso8601 format is correctly set
|
|
28
|
-
#TODO: this should be properly validated in the tsdf library instead
|
|
101
|
+
# TODO: this should be properly validated in the tsdf library instead
|
|
29
102
|
start_date = parser.parse(metadata_time.start_iso8601)
|
|
30
103
|
metadata_time.start_iso8601 = format_datetime_to_iso8601(start_date)
|
|
31
104
|
end_date = parser.parse(metadata_time.end_iso8601)
|
|
32
105
|
metadata_time.end_iso8601 = format_datetime_to_iso8601(end_date)
|
|
33
|
-
start_date = parser.parse(
|
|
34
|
-
|
|
35
|
-
end_date = parser.parse(
|
|
36
|
-
|
|
106
|
+
start_date = parser.parse(metadata_values.start_iso8601)
|
|
107
|
+
metadata_values.start_iso8601 = format_datetime_to_iso8601(start_date)
|
|
108
|
+
end_date = parser.parse(metadata_values.end_iso8601)
|
|
109
|
+
metadata_values.end_iso8601 = format_datetime_to_iso8601(end_date)
|
|
37
110
|
|
|
38
111
|
# TODO: improve the way the metadata is stored at a different location
|
|
39
112
|
metadata_time.file_dir_path = output_path
|
|
40
|
-
|
|
113
|
+
metadata_values.file_dir_path = output_path
|
|
41
114
|
|
|
42
115
|
# store binaries and metadata
|
|
43
|
-
tsdf.write_dataframe_to_binaries(output_path, df, [metadata_time,
|
|
44
|
-
tsdf.write_metadata([metadata_time,
|
|
116
|
+
tsdf.write_dataframe_to_binaries(output_path, df, [metadata_time, metadata_values])
|
|
117
|
+
tsdf.write_metadata([metadata_time, metadata_values], output_filename)
|
|
45
118
|
|
|
46
|
-
|
|
47
|
-
|
|
119
|
+
|
|
120
|
+
def read_metadata(
|
|
121
|
+
input_path: str, meta_filename: str, time_filename: str, values_filename: str
|
|
122
|
+
) -> Tuple[TSDFMetadata, TSDFMetadata]:
|
|
123
|
+
metadata_dict = tsdf.load_metadata_from_path(
|
|
124
|
+
os.path.join(input_path, meta_filename)
|
|
125
|
+
)
|
|
48
126
|
metadata_time = metadata_dict[time_filename]
|
|
49
|
-
|
|
50
|
-
return metadata_time,
|
|
127
|
+
metadata_values = metadata_dict[values_filename]
|
|
128
|
+
return metadata_time, metadata_values
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def load_tsdf_dataframe(path_to_data, prefix, meta_suffix='meta.json', time_suffix='time.bin', values_suffix='values.bin'):
|
|
132
|
+
meta_filename = f"{prefix}_{meta_suffix}"
|
|
133
|
+
time_filename = f"{prefix}_{time_suffix}"
|
|
134
|
+
values_filename = f"{prefix}_{values_suffix}"
|
|
135
|
+
|
|
136
|
+
metadata_time, metadata_values = read_metadata(path_to_data, meta_filename, time_filename, values_filename)
|
|
137
|
+
df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_values], tsdf.constants.ConcatenationType.columns)
|
|
138
|
+
|
|
139
|
+
return df, metadata_time, metadata_values
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def load_metadata_list(
|
|
143
|
+
dir_path: str, meta_filename: str, filenames: List[str]
|
|
144
|
+
) -> List[TSDFMetadata]:
|
|
145
|
+
"""
|
|
146
|
+
Load the metadata objects from a metadata file according to the specified binaries.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
dir_path : str
|
|
151
|
+
The dir path where the metadata file is stored.
|
|
152
|
+
meta_filename : str
|
|
153
|
+
The filename of the metadata file.
|
|
154
|
+
filenames : List[str]
|
|
155
|
+
The list of binary files of which the metadata files need to be loaded
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
metadata_dict = tsdf.load_metadata_from_path(
|
|
159
|
+
os.path.join(dir_path, meta_filename)
|
|
160
|
+
)
|
|
161
|
+
metadata_list = []
|
|
162
|
+
for filename in filenames:
|
|
163
|
+
metadata_list.append(metadata_dict[filename])
|
|
164
|
+
|
|
165
|
+
return metadata_list
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def transform_time_array(
|
|
169
|
+
time_array: pd.Series,
|
|
170
|
+
input_unit_type: str,
|
|
171
|
+
output_unit_type: str,
|
|
172
|
+
start_time: float = 0.0,
|
|
173
|
+
) -> np.ndarray:
|
|
174
|
+
"""
|
|
175
|
+
Transforms the time array to relative time (when defined in delta time) and scales the values.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
time_array : pd.Series
|
|
180
|
+
The time array to transform.
|
|
181
|
+
input_unit_type : str
|
|
182
|
+
The time unit type of the input time array.
|
|
183
|
+
output_unit_type : str
|
|
184
|
+
The time unit type of the output time array. ParaDigMa expects `TimeUnit.RELATIVE_S`.
|
|
185
|
+
start_time : float, optional
|
|
186
|
+
The start time of the time array in UNIX seconds (default is 0.0)
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
np.ndarray
|
|
191
|
+
The transformed time array in seconds, with the specified time unit type.
|
|
192
|
+
|
|
193
|
+
Notes
|
|
194
|
+
-----
|
|
195
|
+
- The function handles different time units (`TimeUnit.RELATIVE_MS`, `TimeUnit.RELATIVE_S`, `TimeUnit.ABSOLUTE_MS`, `TimeUnit.ABSOLUTE_S`, `TimeUnit.DIFFERENCE_MS`, `TimeUnit.DIFFERENCE_S`).
|
|
196
|
+
- The transformation allows for scaling of the time array, converting between time unit types (e.g., relative, absolute, or difference).
|
|
197
|
+
- When converting to `TimeUnit.RELATIVE_MS`, the function calculates the relative time starting from the provided or default start time.
|
|
198
|
+
"""
|
|
199
|
+
input_units = input_unit_type.split('_')[-1].lower()
|
|
200
|
+
output_units = output_unit_type.split('_')[-1].lower()
|
|
201
|
+
|
|
202
|
+
if input_units == output_units:
|
|
203
|
+
scale_factor = 1
|
|
204
|
+
elif input_units == 's' and output_units == 'ms':
|
|
205
|
+
scale_factor = 1e3
|
|
206
|
+
elif input_units == 'ms' and output_units == 's':
|
|
207
|
+
scale_factor = 1 / 1e3
|
|
208
|
+
else:
|
|
209
|
+
raise ValueError(f"Unsupported time units conversion: {input_units} to {output_units}")
|
|
210
|
+
|
|
211
|
+
# Transform to relative time (`TimeUnit.RELATIVE_MS`)
|
|
212
|
+
if input_unit_type == TimeUnit.DIFFERENCE_MS or input_unit_type == TimeUnit.DIFFERENCE_S:
|
|
213
|
+
# Convert a series of differences into cumulative sum to reconstruct original time series.
|
|
214
|
+
time_array = np.cumsum(np.double(time_array))
|
|
215
|
+
elif input_unit_type == TimeUnit.ABSOLUTE_MS or input_unit_type == TimeUnit.ABSOLUTE_S:
|
|
216
|
+
# Set the start time if not provided.
|
|
217
|
+
if np.isclose(start_time, 0.0, rtol=1e-09, atol=1e-09):
|
|
218
|
+
start_time = time_array[0]
|
|
219
|
+
# Convert absolute time stamps into a time series relative to start_time.
|
|
220
|
+
time_array = (time_array - start_time)
|
|
221
|
+
|
|
222
|
+
# Transform the time array from `TimeUnit.RELATIVE_MS` to the specified time unit type
|
|
223
|
+
if output_unit_type == TimeUnit.ABSOLUTE_MS or output_unit_type == TimeUnit.ABSOLUTE_S:
|
|
224
|
+
# Converts time array to absolute time by adding the start time to each element.
|
|
225
|
+
time_array = time_array + start_time
|
|
226
|
+
elif output_unit_type == TimeUnit.DIFFERENCE_MS or output_unit_type == TimeUnit.DIFFERENCE_S:
|
|
227
|
+
# Creates a new array starting with 0, followed by the differences between consecutive elements.
|
|
228
|
+
time_array = np.diff(np.insert(time_array, 0, start_time))
|
|
229
|
+
elif output_unit_type == TimeUnit.RELATIVE_MS or output_unit_type == TimeUnit.RELATIVE_S:
|
|
230
|
+
# The array is already in relative format, do nothing.
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
return time_array * scale_factor
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def convert_units_accelerometer(data: np.ndarray, units: str) -> np.ndarray:
|
|
237
|
+
"""
|
|
238
|
+
Convert acceleration data to g.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
data : np.ndarray
|
|
243
|
+
The acceleration data.
|
|
244
|
+
|
|
245
|
+
units : str
|
|
246
|
+
The unit of the data (currently supports g and m/s^2).
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
np.ndarray
|
|
251
|
+
The acceleration data in g.
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
if units == "m/s^2":
|
|
255
|
+
return data / 9.81
|
|
256
|
+
elif units == "g":
|
|
257
|
+
return data
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError(f"Unsupported unit: {units}")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def convert_units_gyroscope(data: np.ndarray, units: str) -> np.ndarray:
|
|
263
|
+
"""
|
|
264
|
+
Convert gyroscope data to deg/s.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
data : np.ndarray
|
|
269
|
+
The gyroscope data.
|
|
270
|
+
|
|
271
|
+
units : str
|
|
272
|
+
The unit of the data (currently supports deg/s and rad/s).
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
np.ndarray
|
|
277
|
+
The gyroscope data in deg/s.
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
if units == "deg/s":
|
|
281
|
+
return data
|
|
282
|
+
elif units == "rad/s":
|
|
283
|
+
return np.degrees(data)
|
|
284
|
+
else:
|
|
285
|
+
raise ValueError(f"Unsupported unit: {units}")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def invert_watch_side(df: pd.DataFrame, side: str) -> np.ndarray:
|
|
289
|
+
"""
|
|
290
|
+
Invert the data based on the watch side.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
df : pd.DataFrame
|
|
295
|
+
The data.
|
|
296
|
+
side : str
|
|
297
|
+
The watch side (left or right).
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
pd.DataFrame
|
|
302
|
+
The inverted data.
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
if side not in ["left", "right"]:
|
|
306
|
+
raise ValueError(f"Unsupported side: {side}")
|
|
307
|
+
elif side == "right":
|
|
308
|
+
df[DataColumns.GYROSCOPE_Y] *= -1
|
|
309
|
+
df[DataColumns.GYROSCOPE_Z] *= -1
|
|
310
|
+
df[DataColumns.ACCELEROMETER_X] *= -1
|
|
311
|
+
|
|
312
|
+
return df
|
|
313
|
+
|
|
314
|
+
def aggregate_parameter(parameter: np.ndarray, aggregate: str) -> np.ndarray:
|
|
315
|
+
"""
|
|
316
|
+
Aggregate a parameter based on the specified method.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
parameter : np.ndarray
|
|
321
|
+
The parameter to aggregate.
|
|
322
|
+
|
|
323
|
+
aggregate : str
|
|
324
|
+
The aggregation method to apply.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
np.ndarray
|
|
329
|
+
The aggregated parameter.
|
|
330
|
+
"""
|
|
331
|
+
if aggregate == 'mean':
|
|
332
|
+
return np.mean(parameter)
|
|
333
|
+
elif aggregate == 'median':
|
|
334
|
+
return np.median(parameter)
|
|
335
|
+
elif aggregate == 'mode':
|
|
336
|
+
unique_values, counts = np.unique(parameter, return_counts=True)
|
|
337
|
+
return unique_values[np.argmax(counts)]
|
|
338
|
+
elif aggregate == '90p':
|
|
339
|
+
return np.percentile(parameter, 90)
|
|
340
|
+
elif aggregate == '95p':
|
|
341
|
+
return np.percentile(parameter, 95)
|
|
342
|
+
elif aggregate == '99p':
|
|
343
|
+
return np.percentile(parameter, 99)
|
|
344
|
+
elif aggregate == 'std':
|
|
345
|
+
return np.std(parameter)
|
|
346
|
+
else:
|
|
347
|
+
raise ValueError(f"Invalid aggregation method: {aggregate}")
|
|
348
|
+
|
|
349
|
+
def merge_predictions_with_timestamps(
|
|
350
|
+
df_ts: pd.DataFrame,
|
|
351
|
+
df_predictions: pd.DataFrame,
|
|
352
|
+
pred_proba_colname: str,
|
|
353
|
+
window_length_s: float,
|
|
354
|
+
fs: int
|
|
355
|
+
) -> pd.DataFrame:
|
|
356
|
+
"""
|
|
357
|
+
Merges prediction probabilities with timestamps by expanding overlapping windows
|
|
358
|
+
into individual timestamps and averaging probabilities per unique timestamp.
|
|
359
|
+
|
|
360
|
+
Parameters:
|
|
361
|
+
----------
|
|
362
|
+
df_ts : pd.DataFrame
|
|
363
|
+
DataFrame containing timestamps to be merged with predictions.
|
|
364
|
+
Must include the timestamp column specified in `DataColumns.TIME`.
|
|
365
|
+
|
|
366
|
+
df_predictions : pd.DataFrame
|
|
367
|
+
DataFrame containing prediction windows with start times and probabilities.
|
|
368
|
+
Must include:
|
|
369
|
+
- A column for window start times (defined by `DataColumns.TIME`).
|
|
370
|
+
- A column for prediction probabilities (defined by `DataColumns.PRED_GAIT_PROBA`).
|
|
371
|
+
|
|
372
|
+
pred_proba_colname : str
|
|
373
|
+
The column name for the prediction probabilities in `df_predictions`.
|
|
374
|
+
|
|
375
|
+
window_length_s : float
|
|
376
|
+
The length of the prediction window in seconds.
|
|
377
|
+
|
|
378
|
+
fs : int
|
|
379
|
+
The sampling frequency of the data.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
-------
|
|
383
|
+
pd.DataFrame
|
|
384
|
+
Updated `df_ts` with an additional column for averaged prediction probabilities.
|
|
385
|
+
|
|
386
|
+
Steps:
|
|
387
|
+
------
|
|
388
|
+
1. Expand prediction windows into individual timestamps using NumPy broadcasting.
|
|
389
|
+
2. Flatten the timestamps and prediction probabilities into single arrays.
|
|
390
|
+
3. Aggregate probabilities by unique timestamps using pandas `groupby`.
|
|
391
|
+
4. Merge the aggregated probabilities with the input `df_ts`.
|
|
392
|
+
|
|
393
|
+
Notes:
|
|
394
|
+
------
|
|
395
|
+
- Rounding is applied to timestamps to mitigate floating-point inaccuracies.
|
|
396
|
+
- Fully vectorized for speed and scalability, avoiding any row-wise operations.
|
|
397
|
+
"""
|
|
398
|
+
# Step 1: Generate all timestamps for prediction windows using NumPy broadcasting
|
|
399
|
+
window_length = int(window_length_s * fs)
|
|
400
|
+
timestamps = (
|
|
401
|
+
df_predictions[DataColumns.TIME].values[:, None] +
|
|
402
|
+
np.arange(0, window_length) / fs
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Flatten timestamps and probabilities into a single array for efficient processing
|
|
406
|
+
flat_timestamps = timestamps.ravel()
|
|
407
|
+
flat_proba = np.repeat(
|
|
408
|
+
df_predictions[pred_proba_colname].values,
|
|
409
|
+
window_length
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Step 2: Create a DataFrame for expanded data
|
|
413
|
+
expanded_df = pd.DataFrame({
|
|
414
|
+
DataColumns.TIME: flat_timestamps,
|
|
415
|
+
pred_proba_colname: flat_proba
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
# Step 3: Round timestamps and aggregate probabilities
|
|
419
|
+
expanded_df[DataColumns.TIME] = expanded_df[DataColumns.TIME].round(2)
|
|
420
|
+
mean_proba = expanded_df.groupby(DataColumns.TIME, as_index=False).mean()
|
|
421
|
+
|
|
422
|
+
# Step 4: Round timestamps in `df_ts` and merge
|
|
423
|
+
df_ts[DataColumns.TIME] = df_ts[DataColumns.TIME].round(2)
|
|
424
|
+
df_ts = pd.merge(df_ts, mean_proba, how='left', on=DataColumns.TIME)
|
|
425
|
+
df_ts = df_ts.dropna(subset=[pred_proba_colname])
|
|
426
|
+
|
|
427
|
+
return df_ts
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: paradigma
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Paradigma - a toolbox for Digital Biomarkers for Parkinson's Disease
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Author: Erik Post
|
|
7
|
+
Author-email: erik.post@radboudumc.nl
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: pandas (>=2.1.4,<3.0.0)
|
|
15
|
+
Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
|
|
16
|
+
Requires-Dist: pytype (>=2024.4.11,<2025.0.0)
|
|
17
|
+
Requires-Dist: scikit-learn (>=1.3.2,<1.6.1)
|
|
18
|
+
Requires-Dist: tsdf (>=0.5.2,<0.6.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/biomarkersParkinson/paradigma/main/docs/source/_static/img/paradigma-logo-banner.png" alt="ParaDigMa logo"/>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
| Badges | |
|
|
26
|
+
|:----:|----|
|
|
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
|
+
| **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
|
+
| **License** | [](https://github.com/biomarkersparkinson/paradigma/blob/main/LICENSE) |
|
|
31
|
+
<!-- | **Fairness** | [](https://fair-software.eu) [](https://www.bestpractices.dev/projects/8083) | -->
|
|
32
|
+
|
|
33
|
+
## Introduction
|
|
34
|
+
The Parkinsons Disease Digital Markers (ParaDigMa) toolbox is a Python
|
|
35
|
+
software package designed for processing passively collected wrist
|
|
36
|
+
sensor data to extract digital measures of motor and non-motor signs
|
|
37
|
+
of Parkinson's disease (PD).
|
|
38
|
+
|
|
39
|
+
Specifically, the toolbox contains three data processing pipelines:
|
|
40
|
+
(1) arm swing during gait, (2) tremor, and (3) heart rate analysis.
|
|
41
|
+
Furthermore, the toolbox contains general functionalities for signal
|
|
42
|
+
processing and feature extraction, such as filtering, peak detection,
|
|
43
|
+
and spectral analysis. The toolbox is designed to be user-friendly and
|
|
44
|
+
modular, enabling researchers to easily extend the toolbox with new
|
|
45
|
+
algorithms and functionalities. The toolbox is accompanied by a set of
|
|
46
|
+
example scripts and notebooks for each domain that demonstrate how to use
|
|
47
|
+
the toolbox for processing sensor data and extracting digital measures.
|
|
48
|
+
|
|
49
|
+
It contains functionalities for processing the following sensor types:
|
|
50
|
+
|
|
51
|
+
- Inertial Measurement Units (accelerometer, gyroscope)
|
|
52
|
+
- Photoplethysmogram (PPG)
|
|
53
|
+
|
|
54
|
+
## More about ParaDigMa
|
|
55
|
+
The components of ParaDigMa are visually shown in the diagram below.
|
|
56
|
+
|
|
57
|
+
<p align="center">
|
|
58
|
+
<img src="https://raw.githubusercontent.com/biomarkersParkinson/paradigma/main/docs/source/_static/img/pipeline-architecture.png" alt="Pipeline architeecture"/>
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
#### Processes
|
|
62
|
+
ParaDigMa can best be understood by categorizing the sequential processes:
|
|
63
|
+
|
|
64
|
+
| Process | Description |
|
|
65
|
+
| ---- | ---- |
|
|
66
|
+
| Preprocessing | Ensuring that the sensor data is ready for further processing |
|
|
67
|
+
| Feature extraction | Creating features based on windowed views of the timestamps |
|
|
68
|
+
| Classification | Making predictions using developed and validated classifiers |
|
|
69
|
+
| Quantification | Selecting specific features of interest |
|
|
70
|
+
| Aggregation | Aggregating the features at a specified time-level |
|
|
71
|
+
|
|
72
|
+
#### Domain requirements
|
|
73
|
+
ParaDigMa can be used to extract aggregations related to a single or multiple domain(s). Each domain has its specific data requirements. Strict requirements for the domain are marked by X, soft requirements (for some additional functionalities) are marked by O.
|
|
74
|
+
|
|
75
|
+
| | Gait | Tremor | Heart Rate |
|
|
76
|
+
|----------|:-----------:|:-----------:|:-----------:|
|
|
77
|
+
| **Accelerometer** | X | | O |
|
|
78
|
+
| **Gyroscope** | X | X | |
|
|
79
|
+
| **PPG** | | | X |
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
The package is available in PyPi and requires [Python 3.10](https://www.python.org/downloads/) or higher. It can be installed using:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install paradigma
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
See our [extended documentation](https://biomarkersparkinson.github.io/paradigma/).
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
### Installation
|
|
98
|
+
|
|
99
|
+
The package requires Python 3.11 or higher. Use [Poetry](https://python-poetry.org/docs/#installation) to set up the environment and install the dependencies:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
poetry install
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Testing
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
poetry run pytest
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Type checking
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
poetry run pytype .
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Building documentation
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
poetry run make html --directory docs/
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
Interested in contributing? Check out the contributing guidelines. Please note that this project is released with a Code of Conduct. By contributing to this project, you agree to abide by its terms.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
The core team of ParaDigMa consists of Erik Post, Kars Veldkamp, Nienke Timmermans, Diogo Coutinho Soriano, Luc Evers,
|
|
130
|
+
Peter Kok and Vedran Kasalica. Advisors to the project are Max Little, Jordan Raykov, Twan van Laarhoven, Hayriye Cagnan, and Bas Bloem. It is licensed under the terms of the Apache License 2.0 license.
|
|
131
|
+
|
|
132
|
+
## Credits
|
|
133
|
+
|
|
134
|
+
ParaDigMa was created with [`cookiecutter`](https://cookiecutter.readthedocs.io/en/latest/) and the `py-pkgs-cookiecutter` [template](https://github.com/py-pkgs/py-pkgs-cookiecutter).
|
|
135
|
+
|
|
136
|
+
## Contact
|
|
137
|
+
|
|
138
|
+
For more information or questions about ParaDigMa, please reach out to erik.post@radboudumc.nl.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
paradigma/__init__.py,sha256=vCLqo7vOEgcnYs10gUVYvEFfi8y-jBi7w1YKRoqn95k,127
|
|
2
|
+
paradigma/assets/gait_detection_clf_package.pkl,sha256=8jCbuM_4dkilSjOEk9ss7bJbSppgzXe72y0X4BCnzCU,11497247
|
|
3
|
+
paradigma/assets/gait_filtering_clf_package.pkl,sha256=lAaLyhmXdV4X_drmYt0EM6wGwSo80yhpxtncWGq4RfQ,3915
|
|
4
|
+
paradigma/assets/ppg_quality_clf_package.pkl,sha256=vUcM4v8gZwWAmDVK7E4UcHhVnhlEg27RSB71oPGloSc,1292
|
|
5
|
+
paradigma/assets/tremor_detection_clf_package.pkl,sha256=bZ7xrqTsCLg0CKBxVejgnnh8hnKSqNsf-bSQG7D2aqY,1613
|
|
6
|
+
paradigma/classification.py,sha256=sBJSePvwHZNPUQuLdx-pncfnDzMq-1naomsCxSJneWY,2921
|
|
7
|
+
paradigma/config.py,sha256=4F6FsY5HI6d-L_vB6zehyTAJK3X-U0F6IrLwxN9M-dw,11102
|
|
8
|
+
paradigma/constants.py,sha256=JlrD4Zx66g7myQALYAc4Gw_y6yW5EipZuvwj9_fjjpI,3543
|
|
9
|
+
paradigma/feature_extraction.py,sha256=pXfVoiuElrMzLCCZu1gj1uYT5v7vb3vULUjkxeJNWXQ,34566
|
|
10
|
+
paradigma/pipelines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
paradigma/pipelines/gait_pipeline.py,sha256=Go0iUmr82v4Vcv2_WBLp5BqRkxKAXImc2N1EHuFYp40,29058
|
|
12
|
+
paradigma/pipelines/heart_rate_pipeline.py,sha256=0-D9KcW9nwE5sgXsWHONkeKrsX6qZ5BYqjDttoffwL8,17726
|
|
13
|
+
paradigma/pipelines/heart_rate_utils.py,sha256=aV2mTMWrFWHZD0KpHqy3IIC1onZykbppyp7_OUWxFTU,26764
|
|
14
|
+
paradigma/pipelines/tremor_pipeline.py,sha256=hPqLf26MRperwlqpNgrwayByoP3aWwnWc-tQMSJNZGw,13314
|
|
15
|
+
paradigma/preprocessing.py,sha256=DQ-lgmta1tng0neuWftwmXG3yesW6dxOsYvjSV2OFRk,13498
|
|
16
|
+
paradigma/segmenting.py,sha256=Jrz2JQX5eSfR9jBfpBhc6QV0SFmPVT5O6T8MyL0sdSw,13874
|
|
17
|
+
paradigma/testing.py,sha256=DSbWeYl5HuZ-bNyOKwgwMHQGG8KlTabvGTR1Yzd-9CY,17955
|
|
18
|
+
paradigma/util.py,sha256=6G-z6OXv8T2PS8RFjkd6IfDywdzTjvSl1pfNwqnT8i0,14431
|
|
19
|
+
paradigma-0.4.1.dist-info/LICENSE,sha256=Lda8kIVC2kbmlSeYaUWwUwV75Q-q31idYvo18HUTfiw,9807
|
|
20
|
+
paradigma-0.4.1.dist-info/METADATA,sha256=ZEXlKrTQX67H7wM5acfEZIUFJZ9FOSuLVAq9yf6DK_A,6416
|
|
21
|
+
paradigma-0.4.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
22
|
+
paradigma-0.4.1.dist-info/RECORD,,
|