pepbench 0.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.
- pepbench/__init__.py +7 -0
- pepbench/_docutils.py +64 -0
- pepbench/algorithms/__init__.py +16 -0
- pepbench/algorithms/ecg.py +13 -0
- pepbench/algorithms/heartbeat_segmentation.py +5 -0
- pepbench/algorithms/icg.py +29 -0
- pepbench/algorithms/outlier_correction.py +9 -0
- pepbench/data_handling/__init__.py +33 -0
- pepbench/data_handling/_data_handling.py +388 -0
- pepbench/data_handling/utils.py +116 -0
- pepbench/datasets/__init__.py +49 -0
- pepbench/datasets/_base_pep_extraction_dataset.py +253 -0
- pepbench/datasets/_example_dataset.py +141 -0
- pepbench/datasets/_helper.py +128 -0
- pepbench/datasets/empkins/__init__.py +5 -0
- pepbench/datasets/empkins/_dataset.py +480 -0
- pepbench/datasets/empkins/_helper.py +43 -0
- pepbench/datasets/guardian/__init__.py +5 -0
- pepbench/datasets/guardian/_dataset.py +459 -0
- pepbench/datasets/guardian/_helper.py +9 -0
- pepbench/datasets/guardian/_tfm_loader.py +204 -0
- pepbench/evaluation/__init__.py +18 -0
- pepbench/evaluation/_error_metrics.py +86 -0
- pepbench/evaluation/_evaluation.py +227 -0
- pepbench/evaluation/_scoring.py +257 -0
- pepbench/evaluation/_scoring_aggregator.py +18 -0
- pepbench/examples.py +18 -0
- pepbench/export/__init__.py +17 -0
- pepbench/export/_latex.py +161 -0
- pepbench/heartbeat_matching/__init__.py +5 -0
- pepbench/heartbeat_matching/_heartbeat_matching.py +276 -0
- pepbench/io/__init__.py +5 -0
- pepbench/io/_io.py +124 -0
- pepbench/pipelines/__init__.py +27 -0
- pepbench/pipelines/_base_pipeline.py +187 -0
- pepbench/pipelines/_pipeline.py +118 -0
- pepbench/pipelines/_pipeline_reference_b_point.py +115 -0
- pepbench/pipelines/_pipeline_reference_q_peak.py +122 -0
- pepbench/plotting/__init__.py +24 -0
- pepbench/plotting/_base_plotting.py +1005 -0
- pepbench/plotting/_utils.py +618 -0
- pepbench/plotting/algorithms.py +2807 -0
- pepbench/plotting/results.py +917 -0
- pepbench/utils/__init__.py +6 -0
- pepbench/utils/_rename_maps.py +96 -0
- pepbench/utils/_timing.py +33 -0
- pepbench/utils/_types.py +24 -0
- pepbench/utils/exceptions.py +7 -0
- pepbench/utils/styling.py +91 -0
- pepbench-0.1.0.dist-info/METADATA +178 -0
- pepbench-0.1.0.dist-info/RECORD +53 -0
- pepbench-0.1.0.dist-info/WHEEL +4 -0
- pepbench-0.1.0.dist-info/licenses/LICENSE +21 -0
pepbench/__init__.py
ADDED
pepbench/_docutils.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Some small utilities to improve writing docstrings.
|
|
2
|
+
|
|
3
|
+
For now, this just exports some functions from scipy._lib.doccer, to have only one place to import from.
|
|
4
|
+
While, the ``doccer`` submodule of scip[y is not part of the public API, it seems to be stable enough to use it here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
from scipy._lib.doccer import filldoc, inherit_docstring_from
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def filldoc_with_better_error(docdict: dict[str, str]) -> filldoc:
|
|
13
|
+
def inner(func: Callable) -> str:
|
|
14
|
+
try:
|
|
15
|
+
return filldoc(docdict)(func)
|
|
16
|
+
except ValueError as e:
|
|
17
|
+
if "unsupported format character" in str(e):
|
|
18
|
+
raise ValueError(
|
|
19
|
+
"Your docstring contains a single '%' character. "
|
|
20
|
+
"This is not supported by this decorator. "
|
|
21
|
+
"If you want to type a '%' character, you need to type '%%'."
|
|
22
|
+
) from e
|
|
23
|
+
raise
|
|
24
|
+
|
|
25
|
+
return inner
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def make_filldoc(docdict: dict[str, str], *, doc_summary: str | None = None) -> filldoc:
|
|
29
|
+
"""Create a new doc-filler from a dictionary.
|
|
30
|
+
|
|
31
|
+
This can be applied to a function, method, or class to substitute ``%(key)s`` occurrences in its docstring.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
docdict
|
|
36
|
+
Dictionary with docstring keys and values.
|
|
37
|
+
These can be multiline strings and will be dedented before substitution.
|
|
38
|
+
doc_summary
|
|
39
|
+
An optional summary line for the docstring of the returned decorator function.
|
|
40
|
+
This is only relevant for inclusion in the documentation.
|
|
41
|
+
|
|
42
|
+
Note
|
|
43
|
+
----
|
|
44
|
+
If you use this decorator, your docstrings can not contain a single ``%`` character.
|
|
45
|
+
If you want to type a ``%`` character, you need to type ``%%``.
|
|
46
|
+
"""
|
|
47
|
+
inner = filldoc_with_better_error(docdict)
|
|
48
|
+
|
|
49
|
+
if doc_summary is None:
|
|
50
|
+
doc_summary = "Fill docstring from dictionary."
|
|
51
|
+
|
|
52
|
+
full_doc = f"""{doc_summary}
|
|
53
|
+
|
|
54
|
+
Available keys for the substitution::
|
|
55
|
+
|
|
56
|
+
{list(docdict.keys())}
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
inner.__doc__ = full_doc
|
|
60
|
+
inner._dict = docdict
|
|
61
|
+
return inner
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = ["make_filldoc", "inherit_docstring_from"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Individual algorithms for extracting fiducial points from ECG and ICG signals.
|
|
2
|
+
|
|
3
|
+
Together, these algorithms can be combined into a PEP extraction pipeline
|
|
4
|
+
(e.g., :class:`~pepbench.pipelines.PepExtractionPipeline`).
|
|
5
|
+
|
|
6
|
+
The following categories of algorithms are available:
|
|
7
|
+
* :mod:`~pepbench.algorithms.heartbeat_segmentation` - Algorithms for heartbeat segmentation
|
|
8
|
+
* :mod:`~pepbench.algorithms.ecg` - Algorithms for ECG fiducial point extraction
|
|
9
|
+
* :mod:`~pepbench.algorithms.icg` - Algorithms for ICG fiducial point extraction
|
|
10
|
+
* :mod:`~pepbench.algorithms.outlier_correction` - Algorithms for outlier correction of ICG fiducial points
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pepbench.algorithms import ecg, heartbeat_segmentation, icg, outlier_correction
|
|
15
|
+
|
|
16
|
+
__all__ = ["heartbeat_segmentation", "ecg", "icg", "outlier_correction"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Module for ECG event extraction algorithms."""
|
|
2
|
+
|
|
3
|
+
from biopsykit.signals.ecg.event_extraction import (
|
|
4
|
+
QPeakExtractionForouzanfar2018,
|
|
5
|
+
QPeakExtractionMartinez2004Neurokit,
|
|
6
|
+
QPeakExtractionVanLien2013,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"QPeakExtractionForouzanfar2018",
|
|
11
|
+
"QPeakExtractionMartinez2004Neurokit",
|
|
12
|
+
"QPeakExtractionVanLien2013",
|
|
13
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Module for ICG event extraction algorithms."""
|
|
2
|
+
|
|
3
|
+
from biopsykit.signals.icg.event_extraction import (
|
|
4
|
+
BPointExtractionArbol2017IsoelectricCrossings,
|
|
5
|
+
BPointExtractionArbol2017SecondDerivative,
|
|
6
|
+
BPointExtractionArbol2017ThirdDerivative,
|
|
7
|
+
BPointExtractionDebski1993SecondDerivative,
|
|
8
|
+
BPointExtractionDrost2022,
|
|
9
|
+
BPointExtractionForouzanfar2018,
|
|
10
|
+
BPointExtractionLozano2007LinearRegression,
|
|
11
|
+
BPointExtractionLozano2007QuadraticRegression,
|
|
12
|
+
BPointExtractionSherwood1990,
|
|
13
|
+
BPointExtractionStern1985,
|
|
14
|
+
CPointExtractionScipyFindPeaks,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CPointExtractionScipyFindPeaks",
|
|
19
|
+
"BPointExtractionStern1985",
|
|
20
|
+
"BPointExtractionLozano2007LinearRegression",
|
|
21
|
+
"BPointExtractionLozano2007QuadraticRegression",
|
|
22
|
+
"BPointExtractionArbol2017IsoelectricCrossings",
|
|
23
|
+
"BPointExtractionArbol2017ThirdDerivative",
|
|
24
|
+
"BPointExtractionArbol2017SecondDerivative",
|
|
25
|
+
"BPointExtractionSherwood1990",
|
|
26
|
+
"BPointExtractionDrost2022",
|
|
27
|
+
"BPointExtractionDebski1993SecondDerivative",
|
|
28
|
+
"BPointExtractionForouzanfar2018",
|
|
29
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Module for ICG outlier correction algorithms."""
|
|
2
|
+
|
|
3
|
+
from biopsykit.signals.icg.outlier_correction import (
|
|
4
|
+
OutlierCorrectionDummy,
|
|
5
|
+
OutlierCorrectionForouzanfar2018,
|
|
6
|
+
OutlierCorrectionLinearInterpolation,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = ["OutlierCorrectionDummy", "OutlierCorrectionLinearInterpolation", "OutlierCorrectionForouzanfar2018"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Module for various data handling helper functions."""
|
|
2
|
+
|
|
3
|
+
from pepbench.data_handling import utils
|
|
4
|
+
from pepbench.data_handling._data_handling import (
|
|
5
|
+
add_unique_id_to_results_dataframe,
|
|
6
|
+
compute_improvement_outlier_correction,
|
|
7
|
+
compute_improvement_pipeline,
|
|
8
|
+
compute_pep_performance_metrics,
|
|
9
|
+
correlation_reference_pep_heart_rate,
|
|
10
|
+
describe_pep_values,
|
|
11
|
+
get_data_for_algo,
|
|
12
|
+
get_error_by_group,
|
|
13
|
+
get_pep_for_algo,
|
|
14
|
+
get_reference_data,
|
|
15
|
+
get_reference_pep,
|
|
16
|
+
rr_interval_to_heart_rate,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"get_reference_pep",
|
|
21
|
+
"get_error_by_group",
|
|
22
|
+
"describe_pep_values",
|
|
23
|
+
"get_pep_for_algo",
|
|
24
|
+
"get_reference_data",
|
|
25
|
+
"get_data_for_algo",
|
|
26
|
+
"rr_interval_to_heart_rate",
|
|
27
|
+
"compute_pep_performance_metrics",
|
|
28
|
+
"correlation_reference_pep_heart_rate",
|
|
29
|
+
"utils",
|
|
30
|
+
"add_unique_id_to_results_dataframe",
|
|
31
|
+
"compute_improvement_outlier_correction",
|
|
32
|
+
"compute_improvement_pipeline",
|
|
33
|
+
]
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pingouin as pg
|
|
6
|
+
|
|
7
|
+
from pepbench.utils._types import str_t
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"get_error_by_group",
|
|
11
|
+
"get_reference_data",
|
|
12
|
+
"get_reference_pep",
|
|
13
|
+
"get_pep_for_algo",
|
|
14
|
+
"get_data_for_algo",
|
|
15
|
+
"describe_pep_values",
|
|
16
|
+
"compute_pep_performance_metrics",
|
|
17
|
+
"rr_interval_to_heart_rate",
|
|
18
|
+
"correlation_reference_pep_heart_rate",
|
|
19
|
+
"add_unique_id_to_results_dataframe",
|
|
20
|
+
"compute_improvement_outlier_correction",
|
|
21
|
+
"compute_improvement_pipeline",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_algo_levels = ["q_peak_algorithm", "b_point_algorithm", "outlier_correction_algorithm"]
|
|
26
|
+
|
|
27
|
+
_pep_error_metric_map = {
|
|
28
|
+
"absolute_error_per_sample_ms": "Mean Absolute Error [ms]",
|
|
29
|
+
"error_per_sample_ms": "Mean Error [ms]",
|
|
30
|
+
"absolute_relative_error_per_sample_percent": "Mean Absolute Relative Error [%]",
|
|
31
|
+
}
|
|
32
|
+
_pep_number_map = {
|
|
33
|
+
"num_pep_valid": "Valid PEPs",
|
|
34
|
+
"num_pep_invalid": "Invalid PEPs",
|
|
35
|
+
"num_pep_total": "Total PEPs",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_reference_data(results_per_sample: pd.DataFrame) -> pd.DataFrame:
|
|
40
|
+
"""Extract the reference data from the *results-per-sample* dataframe.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
results_per_sample : :class:`pandas.DataFrame`
|
|
45
|
+
The results-per-sample dataframe.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
:class:`pandas.DataFrame`
|
|
50
|
+
The reference data.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
reference_pep = results_per_sample.xs("reference", level=-1, axis=1)
|
|
54
|
+
reference_pep = reference_pep.groupby(_algo_levels)
|
|
55
|
+
reference_pep = reference_pep.get_group(next(iter(reference_pep.groups))).droplevel(_algo_levels)
|
|
56
|
+
|
|
57
|
+
return reference_pep
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_reference_pep(results_per_sample: pd.DataFrame) -> pd.DataFrame:
|
|
61
|
+
"""Extract the reference PEP values from the *results-per-sample* dataframe.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
results_per_sample : :class:`pandas.DataFrame`
|
|
66
|
+
The results-per-sample dataframe.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
:class:`pandas.DataFrame`
|
|
71
|
+
The reference PEP values.
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
return get_reference_data(results_per_sample)[["pep_ms"]]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_data_for_algo(results_per_sample: pd.DataFrame, algo_combi: str_t) -> pd.DataFrame:
|
|
78
|
+
"""Extract the data for a specific algorithm combination from the *results-per-sample* dataframe.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
results_per_sample : :class:`pandas.DataFrame`
|
|
83
|
+
The results-per-sample dataframe.
|
|
84
|
+
algo_combi : str or tuple of str
|
|
85
|
+
The algorithm combination for which the data should be extracted.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
:class:`pandas.DataFrame`
|
|
90
|
+
The data for the specified algorithm combination.
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
algo_levels = [s for s in results_per_sample.index.names if s in _algo_levels]
|
|
94
|
+
if isinstance(algo_combi, str):
|
|
95
|
+
algo_combi = (algo_combi,)
|
|
96
|
+
data = results_per_sample.xs(tuple(algo_combi), level=algo_levels)
|
|
97
|
+
return data
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_pep_for_algo(results_per_sample: pd.DataFrame, algo_combi: Sequence[str]) -> pd.DataFrame:
|
|
101
|
+
"""Extract the PEP values for a specific algorithm combination from the *results-per-sample* dataframe.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
results_per_sample : :class:`pandas.DataFrame`
|
|
106
|
+
The results-per-sample dataframe.
|
|
107
|
+
algo_combi : str or tuple of str
|
|
108
|
+
The algorithm combination for which the PEP values should be extracted.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
:class:`pandas.DataFrame`
|
|
113
|
+
The PEP values for the specified algorithm combination.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
pep = get_data_for_algo(results_per_sample, algo_combi)
|
|
117
|
+
pep = pep[[("pep_ms", "estimated")]].droplevel(level=-1, axis=1)
|
|
118
|
+
|
|
119
|
+
return pep
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def describe_pep_values(
|
|
123
|
+
data: pd.DataFrame, group_cols: str_t | None = None, metrics: Sequence[str] | None = None
|
|
124
|
+
) -> pd.DataFrame:
|
|
125
|
+
"""Compute the descriptive statistics for the PEP values using the :meth:`pandas.DataFrame.describe` method.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
data : :class:`pandas.DataFrame`
|
|
130
|
+
The PEP values.
|
|
131
|
+
group_cols : str or list of str, optional
|
|
132
|
+
The column(s) to group the data by, if any. Default: "phase".
|
|
133
|
+
metrics : list of str, optional
|
|
134
|
+
List of metrics to display from the descriptive statistics. Default: ["mean", "std", "min", "max"].
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
if group_cols is None:
|
|
138
|
+
group_cols = ["phase"]
|
|
139
|
+
if metrics is None:
|
|
140
|
+
metrics = ["mean", "std", "min", "max"]
|
|
141
|
+
|
|
142
|
+
return data.groupby(group_cols).describe().reindex(metrics, level=-1, axis=1)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def compute_pep_performance_metrics(
|
|
146
|
+
results_per_sample: pd.DataFrame,
|
|
147
|
+
*,
|
|
148
|
+
num_heartbeats: pd.DataFrame | None = None,
|
|
149
|
+
metrics: Sequence[str] | None = None,
|
|
150
|
+
sortby: str_t | None = ("absolute_error_per_sample_ms", "mean"),
|
|
151
|
+
ascending: bool | None = True,
|
|
152
|
+
) -> pd.DataFrame:
|
|
153
|
+
"""Compute the performance metrics for the PEP values.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
results_per_sample : :class:`pandas.DataFrame`
|
|
158
|
+
The results-per-sample dataframe.
|
|
159
|
+
num_heartbeats : :class:`pandas.DataFrame`, optional
|
|
160
|
+
Dataframe containing the number of heartbeats (to include in the output). Default: None.
|
|
161
|
+
metrics : list of str, optional
|
|
162
|
+
List of metrics to compute. Default: ["mean", "std"].
|
|
163
|
+
sortby : str, optional
|
|
164
|
+
The column to sort the results by. Default: ("absolute_error_per_sample_ms", "mean").
|
|
165
|
+
ascending : bool, optional
|
|
166
|
+
Whether to sort the results in ascending order. Default: True.
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
if metrics is None:
|
|
170
|
+
metrics = ["mean", "std"]
|
|
171
|
+
results_per_sample = results_per_sample.copy()
|
|
172
|
+
algo_levels = [s for s in results_per_sample.index.names if s in _algo_levels]
|
|
173
|
+
results_per_sample = results_per_sample[_pep_error_metric_map.keys()].droplevel(level=-1, axis=1)
|
|
174
|
+
results_per_sample = results_per_sample.groupby(algo_levels)
|
|
175
|
+
results_per_sample = results_per_sample.agg(metrics)
|
|
176
|
+
|
|
177
|
+
num_heartbeats = num_heartbeats.unstack().swaplevel(axis=1)
|
|
178
|
+
results_per_sample = results_per_sample.join(num_heartbeats)
|
|
179
|
+
|
|
180
|
+
if sortby is not None:
|
|
181
|
+
results_per_sample = results_per_sample.sort_values(sortby, ascending=ascending)
|
|
182
|
+
|
|
183
|
+
rename_map = _pep_error_metric_map.copy()
|
|
184
|
+
rename_map.update(_pep_number_map)
|
|
185
|
+
results_per_sample = results_per_sample.rename(rename_map, level=0, axis=1)
|
|
186
|
+
results_per_sample = results_per_sample.reindex(rename_map.values(), level=0, axis=1)
|
|
187
|
+
|
|
188
|
+
return results_per_sample
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_performance_metric(results_per_sample: pd.DataFrame, metric: str) -> pd.DataFrame:
|
|
192
|
+
"""Extract a specific performance metric from the *results-per-sample* dataframe.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
results_per_sample : :class:`pandas.DataFrame`
|
|
197
|
+
The results-per-sample dataframe.
|
|
198
|
+
metric : str
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
:class:`pandas.DataFrame`
|
|
203
|
+
The extracted performance metric.
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
return results_per_sample[[metric]].droplevel(level=-1, axis=1)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def rr_interval_to_heart_rate(data: pd.DataFrame) -> pd.DataFrame:
|
|
210
|
+
"""Convert RR intervals in milliseconds to heart rate in beats per minute.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
data : :class:`pandas.DataFrame`
|
|
215
|
+
The data containing the RR intervals in milliseconds. The column name must be "rr_interval_ms".
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
:class:`pandas.DataFrame`
|
|
220
|
+
The data with the heart rate in beats per minute
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
heart_rate_bpm = 60 * 1000 / data[["rr_interval_ms"]]
|
|
224
|
+
heart_rate_bpm = heart_rate_bpm.rename(columns={"rr_interval_ms": "heart_rate_bpm"})
|
|
225
|
+
return data.join(heart_rate_bpm)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def correlation_reference_pep_heart_rate(data: pd.DataFrame) -> dict[str, pd.DataFrame]:
|
|
229
|
+
"""Compute the correlation between the reference PEP values and the heart rate.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
data : :class:`pandas.DataFrame`
|
|
234
|
+
The data containing the reference PEP values and the heart rate.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
dict
|
|
239
|
+
A dictionary containing the linear regression model and the correlation coefficient.
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
data = get_reference_data(data)
|
|
243
|
+
|
|
244
|
+
# compute a linear regression model
|
|
245
|
+
linreg = pg.linear_regression(X=data["heart_rate_bpm"], y=data["pep_ms"], remove_na=True)
|
|
246
|
+
corr = pg.corr(data["heart_rate_bpm"], data["pep_ms"], method="pearson")
|
|
247
|
+
|
|
248
|
+
return {"linear_regression": linreg, "correlation": corr}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_error_by_group(
|
|
252
|
+
results_per_sample: pd.DataFrame, error_metric: str = "absolute_error_per_sample_ms", grouper: str_t = "participant"
|
|
253
|
+
) -> pd.DataFrame:
|
|
254
|
+
"""Compute mean and standard deviation of the error metric by group.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
results_per_sample : :class:`pandas.DataFrame`
|
|
259
|
+
The results-per-sample dataframe.
|
|
260
|
+
error_metric : str, optional
|
|
261
|
+
The error metric to extract. Default: "absolute_error_per_sample_ms".
|
|
262
|
+
grouper : str or list of str, optional
|
|
263
|
+
The column(s) to group the data by. Default: "participant".
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
:class:`pandas.DataFrame`
|
|
268
|
+
The error metric aggregated by group.
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
algo_levels = [s for s in results_per_sample.index.names if s in _algo_levels]
|
|
272
|
+
if isinstance(grouper, str):
|
|
273
|
+
grouper = [grouper]
|
|
274
|
+
|
|
275
|
+
error = results_per_sample[[error_metric]].groupby([*algo_levels, *grouper]).agg(["mean", "std"])
|
|
276
|
+
error = (
|
|
277
|
+
error.droplevel(1, axis=1)
|
|
278
|
+
.unstack(algo_levels)
|
|
279
|
+
.reorder_levels([0, *range(2, 2 + len(algo_levels)), 1], axis=1)
|
|
280
|
+
.sort_index(axis=1)
|
|
281
|
+
)
|
|
282
|
+
error.columns = error.columns.set_names("metric", level=0)
|
|
283
|
+
return error
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def add_unique_id_to_results_dataframe(data: pd.DataFrame, algo_levels: Sequence[str] | None = None) -> pd.DataFrame:
|
|
287
|
+
"""Add a unique ID to the results dataframe.
|
|
288
|
+
|
|
289
|
+
The unique ID is created by concatenating the values of the specified algorithm levels and the heartbeat IDs.
|
|
290
|
+
This is then added as a new index level named "id_concat".
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
data : :class:`pandas.DataFrame`
|
|
295
|
+
The results dataframe.
|
|
296
|
+
algo_levels : list of str, optional
|
|
297
|
+
The algorithm levels to use for the unique ID. If None, the default algorithm levels
|
|
298
|
+
(["q_peak_algorithm", "b_point_algorithm", "outlier_correction_algorithm"]) are used.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
:class:`pandas.DataFrame`
|
|
303
|
+
The results dataframe with the unique IDs added as new index level.
|
|
304
|
+
|
|
305
|
+
"""
|
|
306
|
+
data = data.copy()
|
|
307
|
+
data = data.droplevel(axis=1, level=-1).rename(index=str)
|
|
308
|
+
if algo_levels is None:
|
|
309
|
+
algo_levels = _algo_levels
|
|
310
|
+
if isinstance(algo_levels, str):
|
|
311
|
+
algo_levels = [algo_levels]
|
|
312
|
+
|
|
313
|
+
algo_levels = [s for s in data.index.names if s in algo_levels]
|
|
314
|
+
data = data.reset_index(level=algo_levels)
|
|
315
|
+
id_concat = pd.Index(["_".join(i) for i in data.index], name="id_concat")
|
|
316
|
+
data = data.assign(id_concat=id_concat)
|
|
317
|
+
data = data.set_index([*algo_levels, "id_concat"])
|
|
318
|
+
return data
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def compute_improvement_outlier_correction(data: pd.DataFrame, outlier_algos: Sequence[str]) -> pd.DataFrame:
|
|
322
|
+
"""Compute the percentage of samples which improved, deteriorated, or remained unchanged after outlier correction.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
data : :class:`pandas.DataFrame`
|
|
327
|
+
The data containing the PEP values before and after outlier correction.
|
|
328
|
+
outlier_algos : list of str
|
|
329
|
+
The outlier correction algorithms to consider.
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
:class:`pandas.DataFrame`
|
|
334
|
+
The percentage of samples which improved, deteriorated, or remained unchanged after outlier correction.
|
|
335
|
+
|
|
336
|
+
"""
|
|
337
|
+
data = data.copy()
|
|
338
|
+
if "outlier_correction_algorithm" not in data.columns.names:
|
|
339
|
+
data = data.unstack("outlier_correction_algorithm")
|
|
340
|
+
data = data.reindex(outlier_algos, axis=1, level="outlier_correction_algorithm")
|
|
341
|
+
data = data.diff(axis=1).dropna(how="all", axis=1)
|
|
342
|
+
data = np.sign(data)
|
|
343
|
+
data.columns = ["improvement_percent"]
|
|
344
|
+
# negative values = improvement through outlier correction
|
|
345
|
+
# count the number of positive and negative values
|
|
346
|
+
improvement_percent = data.value_counts(normalize=True) * 100
|
|
347
|
+
improvement_percent = improvement_percent.rename({-1: "improvement", 1: "deterioration", 0: "no change"})
|
|
348
|
+
improvement_percent = improvement_percent.to_frame().reindex(["improvement", "no change", "deterioration"], level=0)
|
|
349
|
+
improvement_percent.columns = ["improvement_percent"]
|
|
350
|
+
improvement_percent.index.names = [None]
|
|
351
|
+
improvement_percent = improvement_percent.T
|
|
352
|
+
return improvement_percent
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def compute_improvement_pipeline(data: pd.DataFrame, pipelines: Sequence[str]) -> pd.DataFrame:
|
|
356
|
+
"""Compute the percentage of samples which showed sign changes in the error metric between two pipelines.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
data : :class:`pandas.DataFrame`
|
|
361
|
+
The data containing the PEP extraction results from different pipelines.
|
|
362
|
+
pipelines : list of str
|
|
363
|
+
The pipelines to compare.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
:class:`pandas.DataFrame`
|
|
368
|
+
Overview of the percentage of samples which showed a change in the sign of the error metric
|
|
369
|
+
(i.e., either positive to negative, vice versa, or no change) between two pipelines.
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
data = data.copy()
|
|
373
|
+
pipelines = ["_".join(i) for i in pipelines]
|
|
374
|
+
|
|
375
|
+
data = data.unstack("pipeline").reindex(pipelines, level="pipeline", axis=1)
|
|
376
|
+
# compute the percentage of sample which have a positive value in the first column
|
|
377
|
+
# and a negative value in the second column
|
|
378
|
+
data = data.assign(change_pos_neg=(data.iloc[:, 0] > 0) & (data.iloc[:, 1] < 0))
|
|
379
|
+
data = data.assign(change_pos_pos=(data.iloc[:, 0] > 0) & (data.iloc[:, 1] > 0))
|
|
380
|
+
data = data.assign(change_neg_pos=(data.iloc[:, 0] < 0) & (data.iloc[:, 1] > 0))
|
|
381
|
+
data = data.assign(change_neg_neg=(data.iloc[:, 0] < 0) & (data.iloc[:, 1] < 0))
|
|
382
|
+
data = data.assign(change_diff=data["change_pos_neg"] | data["change_neg_pos"])
|
|
383
|
+
data = data.assign(change_same=data["change_pos_pos"] | data["change_neg_neg"])
|
|
384
|
+
|
|
385
|
+
data = data.filter(like="change", axis=1)
|
|
386
|
+
data = data.apply(pd.Series.value_counts, normalize=True) * 100
|
|
387
|
+
|
|
388
|
+
return data
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Utility functions for data handling."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
__all__ = ["reindex_empkins", "rename_empkins", "reindex_guardian", "rename_guardian"]
|
|
6
|
+
|
|
7
|
+
condition_mapping_empkins = {"tsst": "TSST", "ftsst": "f-TSST"}
|
|
8
|
+
phase_mapping_empkins = {
|
|
9
|
+
"Prep": "Preparation",
|
|
10
|
+
"Pause_1": "Pause 1",
|
|
11
|
+
"Talk": "Talk",
|
|
12
|
+
"Math": "Math",
|
|
13
|
+
"Pause_5": "Pause 5",
|
|
14
|
+
}
|
|
15
|
+
phase_mapping_guardian = {
|
|
16
|
+
"Pause": "Pause",
|
|
17
|
+
"Valsalva": "Valsalva",
|
|
18
|
+
"HoldingBreath": "Apnea",
|
|
19
|
+
"TiltUp": "Tilt-Up",
|
|
20
|
+
"TiltDown": "Tilt-Down",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def reindex_empkins(data: pd.DataFrame, after_rename: bool = False) -> pd.DataFrame:
|
|
25
|
+
"""Reindex data from the *EmpkinSDataset*.
|
|
26
|
+
|
|
27
|
+
The reindexing is performed according to the condition (tsst, ftsst) and phase (Prep, Pause_1, Talk, Math, Pause_5)
|
|
28
|
+
mappings. The reindexing can be performed *before* or *after* the data has been renamed
|
|
29
|
+
(using :func:`rename_empkins`).
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
data : :class:`pandas.DataFrame`
|
|
34
|
+
Data from the *EmpkinSDataset*.
|
|
35
|
+
after_rename : bool, optional
|
|
36
|
+
``True`` if the data has already been renamed using :func:`rename_empkins`, ``False`` otherwise.
|
|
37
|
+
Default: ``False``
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
:class:`pandas.DataFrame`
|
|
42
|
+
Reindexed data from the *EmpkinSDataset*.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
if after_rename:
|
|
46
|
+
return data.reindex(condition_mapping_empkins.values(), level="condition").reindex(
|
|
47
|
+
phase_mapping_empkins.values(), level="phase"
|
|
48
|
+
)
|
|
49
|
+
return data.reindex(condition_mapping_empkins.keys(), level="condition").reindex(
|
|
50
|
+
phase_mapping_empkins.keys(), level="phase"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def rename_empkins(data: pd.DataFrame) -> pd.DataFrame:
|
|
55
|
+
"""Rename the data from the *EmpkinSDataset*.
|
|
56
|
+
|
|
57
|
+
The renaming is performed according to the condition (tsst -> TSST, ftsst -> f-TSST) and phase
|
|
58
|
+
(Prep -> Preparation, Pause_1 -> Pause 1, Talk -> Talk, Math -> Math, Pause_5 -> Pause 5) mappings.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
data : :class:`pandas.DataFrame`
|
|
63
|
+
Data from the *EmpkinSDataset*.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
:class:`pandas.DataFrame`
|
|
68
|
+
Renamed data from the *EmpkinSDataset*.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
return data.rename(condition_mapping_empkins, level="condition").rename(phase_mapping_empkins, level="phase")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def reindex_guardian(data: pd.DataFrame, after_rename: bool | None = False) -> pd.DataFrame:
|
|
75
|
+
"""Reindex data from the *GuardianDataset*.
|
|
76
|
+
|
|
77
|
+
The reindexing is performed according to the phase (Pause, Valsalva, HoldingBreath, TiltUp, TiltDown) mappings.
|
|
78
|
+
The reindexing can be performed *before* or *after* the data has been renamed (using :func:`rename_guardian`).
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
data : :class:`pandas.DataFrame`
|
|
83
|
+
Data from the *GuardianDataset*.
|
|
84
|
+
after_rename : bool, optional
|
|
85
|
+
``True`` if the data has already been renamed using :func:`rename_guardian`, ``False`` otherwise.
|
|
86
|
+
Default: ``False``
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
:class:`pandas.DataFrame`
|
|
91
|
+
Reindexed data from the *GuardianDataset*.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
if after_rename:
|
|
95
|
+
return data.reindex(phase_mapping_guardian.values(), level="phase")
|
|
96
|
+
return data.reindex(phase_mapping_guardian.keys(), level="phase")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def rename_guardian(data: pd.DataFrame) -> pd.DataFrame:
|
|
100
|
+
"""Rename the data from the *GuardianDataset*.
|
|
101
|
+
|
|
102
|
+
The renaming is performed according to the phase (Pause -> Pause, Valsalva -> Valsalva, HoldingBreath -> Apnea,
|
|
103
|
+
TiltUp -> Tilt-Up, TiltDown -> Tilt-Down) mappings.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
data : :class:`pandas.DataFrame`
|
|
108
|
+
Data from the *GuardianDataset*.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
:class:`pandas.DataFrame`
|
|
113
|
+
Renamed data from the *GuardianDataset*.
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
return data.rename(phase_mapping_guardian, level="phase")
|