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.
Files changed (53) hide show
  1. pepbench/__init__.py +7 -0
  2. pepbench/_docutils.py +64 -0
  3. pepbench/algorithms/__init__.py +16 -0
  4. pepbench/algorithms/ecg.py +13 -0
  5. pepbench/algorithms/heartbeat_segmentation.py +5 -0
  6. pepbench/algorithms/icg.py +29 -0
  7. pepbench/algorithms/outlier_correction.py +9 -0
  8. pepbench/data_handling/__init__.py +33 -0
  9. pepbench/data_handling/_data_handling.py +388 -0
  10. pepbench/data_handling/utils.py +116 -0
  11. pepbench/datasets/__init__.py +49 -0
  12. pepbench/datasets/_base_pep_extraction_dataset.py +253 -0
  13. pepbench/datasets/_example_dataset.py +141 -0
  14. pepbench/datasets/_helper.py +128 -0
  15. pepbench/datasets/empkins/__init__.py +5 -0
  16. pepbench/datasets/empkins/_dataset.py +480 -0
  17. pepbench/datasets/empkins/_helper.py +43 -0
  18. pepbench/datasets/guardian/__init__.py +5 -0
  19. pepbench/datasets/guardian/_dataset.py +459 -0
  20. pepbench/datasets/guardian/_helper.py +9 -0
  21. pepbench/datasets/guardian/_tfm_loader.py +204 -0
  22. pepbench/evaluation/__init__.py +18 -0
  23. pepbench/evaluation/_error_metrics.py +86 -0
  24. pepbench/evaluation/_evaluation.py +227 -0
  25. pepbench/evaluation/_scoring.py +257 -0
  26. pepbench/evaluation/_scoring_aggregator.py +18 -0
  27. pepbench/examples.py +18 -0
  28. pepbench/export/__init__.py +17 -0
  29. pepbench/export/_latex.py +161 -0
  30. pepbench/heartbeat_matching/__init__.py +5 -0
  31. pepbench/heartbeat_matching/_heartbeat_matching.py +276 -0
  32. pepbench/io/__init__.py +5 -0
  33. pepbench/io/_io.py +124 -0
  34. pepbench/pipelines/__init__.py +27 -0
  35. pepbench/pipelines/_base_pipeline.py +187 -0
  36. pepbench/pipelines/_pipeline.py +118 -0
  37. pepbench/pipelines/_pipeline_reference_b_point.py +115 -0
  38. pepbench/pipelines/_pipeline_reference_q_peak.py +122 -0
  39. pepbench/plotting/__init__.py +24 -0
  40. pepbench/plotting/_base_plotting.py +1005 -0
  41. pepbench/plotting/_utils.py +618 -0
  42. pepbench/plotting/algorithms.py +2807 -0
  43. pepbench/plotting/results.py +917 -0
  44. pepbench/utils/__init__.py +6 -0
  45. pepbench/utils/_rename_maps.py +96 -0
  46. pepbench/utils/_timing.py +33 -0
  47. pepbench/utils/_types.py +24 -0
  48. pepbench/utils/exceptions.py +7 -0
  49. pepbench/utils/styling.py +91 -0
  50. pepbench-0.1.0.dist-info/METADATA +178 -0
  51. pepbench-0.1.0.dist-info/RECORD +53 -0
  52. pepbench-0.1.0.dist-info/WHEEL +4 -0
  53. pepbench-0.1.0.dist-info/licenses/LICENSE +21 -0
pepbench/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Top-level module for the PEPBench package."""
2
+
3
+ from pepbench import algorithms, datasets, evaluation, pipelines
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["algorithms", "datasets", "pipelines", "evaluation"]
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,5 @@
1
+ """Module for heartbeat segmentation algorithms based on ECG data."""
2
+
3
+ from biopsykit.signals.ecg.segmentation import HeartbeatSegmentationNeurokit
4
+
5
+ __all__ = ["HeartbeatSegmentationNeurokit"]
@@ -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")