featuregraph-smoothing-core 1.0.0__tar.gz

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 (30) hide show
  1. featuregraph_smoothing_core-1.0.0/LICENSE +21 -0
  2. featuregraph_smoothing_core-1.0.0/PKG-INFO +69 -0
  3. featuregraph_smoothing_core-1.0.0/README.md +54 -0
  4. featuregraph_smoothing_core-1.0.0/pyproject.toml +23 -0
  5. featuregraph_smoothing_core-1.0.0/setup.cfg +4 -0
  6. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/__init__.py +10 -0
  7. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/behaviors/__init__.py +0 -0
  8. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/behaviors/oscillation.py +101 -0
  9. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/datasets/__init__.py +3 -0
  10. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/datasets/_bidmc.py +44 -0
  11. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/operators/__init__.py +0 -0
  12. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/operators/events.py +27 -0
  13. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/operators/measures.py +13 -0
  14. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/operators/states.py +30 -0
  15. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/plot.py +460 -0
  16. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/utils/__init__.py +0 -0
  17. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/utils/_bidmc.py +215 -0
  18. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/utils/_rename_map.py +23 -0
  19. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/validation/__init__.py +15 -0
  20. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/validation/canonical.py +76 -0
  21. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/validation/characterize.py +61 -0
  22. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/validation/exclusions.py +14 -0
  23. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core/validation/matching.py +42 -0
  24. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core.egg-info/PKG-INFO +69 -0
  25. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core.egg-info/SOURCES.txt +28 -0
  26. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core.egg-info/dependency_links.txt +1 -0
  27. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core.egg-info/requires.txt +4 -0
  28. featuregraph_smoothing_core-1.0.0/src/featuregraph_smoothing_core.egg-info/top_level.txt +1 -0
  29. featuregraph_smoothing_core-1.0.0/tests/test_oscillation.py +114 -0
  30. featuregraph_smoothing_core-1.0.0/tests/test_validation.py +174 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nazia Habib
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: featuregraph-smoothing-core
3
+ Version: 1.0.0
4
+ Summary: Minimal, frozen subset of FeatureGraph's state-detection code, extracted to support reproducibility of the smoothing-paper results.
5
+ Author: Nazia Habib
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pandas>=1.5
11
+ Requires-Dist: numpy>=1.23
12
+ Requires-Dist: matplotlib>=3.5
13
+ Requires-Dist: requests>=2.25
14
+ Dynamic: license-file
15
+
16
+ # featuregraph-smoothing-core
17
+
18
+ Code and data behind "A Compiler-Level Account of Smoothing-Parameter
19
+ Choice." Contains `OscillationConfig` (the construction used throughout
20
+ the paper), a BIDMC data loader, and the validation logic behind the
21
+ paper's human-annotation results.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install -e .
27
+ ```
28
+
29
+ ## Reproduce the paper's results
30
+
31
+ ```bash
32
+ pytest tests/ -v
33
+ ```
34
+
35
+ Two tests are skipped by default, since they need a live download from
36
+ PhysioNet. Their skip reasons record the exact confirmed results;
37
+ remove the `@pytest.mark.skip` decorator above either one and rerun to
38
+ reproduce it directly:
39
+
40
+ - `test_reproduces_paper_correlation_and_ratio_range` — the paper's
41
+ population correlation (0.39) and ratio range (1.14x–40.25x) across
42
+ all 53 BIDMC subjects.
43
+ - `test_bidmc_peak_matched_recall_excluding_flagged_subjects` — the
44
+ paper's human-annotation recall results (Table 1), N=32.
45
+
46
+ ## Using OscillationConfig directly
47
+
48
+ ```python
49
+ from featuregraph_smoothing_core.behaviors.oscillation import OscillationConfig
50
+
51
+ config = OscillationConfig(signal="respiration", smooth_window=100)
52
+ added = config.add_primitives(df, "subject")
53
+ summary = config.summarize(added, ["subject", config.trough_event_id_col])
54
+ ```
55
+
56
+ ## What's here
57
+
58
+ - `src/featuregraph_smoothing_core/` — the package
59
+ - `notebooks/bidmc_visual_demo.ipynb` — the code that produced Figure 1
60
+ - `artifacts/paper/compiler/smoothing.md` — the paper manuscript
61
+ - `tests/` — the full test suite
62
+
63
+ ## Citation
64
+
65
+ Software: https://doi.org/10.5281/zenodo.22947447
66
+
67
+ ## License
68
+
69
+ MIT. See `LICENSE`.
@@ -0,0 +1,54 @@
1
+ # featuregraph-smoothing-core
2
+
3
+ Code and data behind "A Compiler-Level Account of Smoothing-Parameter
4
+ Choice." Contains `OscillationConfig` (the construction used throughout
5
+ the paper), a BIDMC data loader, and the validation logic behind the
6
+ paper's human-annotation results.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install -e .
12
+ ```
13
+
14
+ ## Reproduce the paper's results
15
+
16
+ ```bash
17
+ pytest tests/ -v
18
+ ```
19
+
20
+ Two tests are skipped by default, since they need a live download from
21
+ PhysioNet. Their skip reasons record the exact confirmed results;
22
+ remove the `@pytest.mark.skip` decorator above either one and rerun to
23
+ reproduce it directly:
24
+
25
+ - `test_reproduces_paper_correlation_and_ratio_range` — the paper's
26
+ population correlation (0.39) and ratio range (1.14x–40.25x) across
27
+ all 53 BIDMC subjects.
28
+ - `test_bidmc_peak_matched_recall_excluding_flagged_subjects` — the
29
+ paper's human-annotation recall results (Table 1), N=32.
30
+
31
+ ## Using OscillationConfig directly
32
+
33
+ ```python
34
+ from featuregraph_smoothing_core.behaviors.oscillation import OscillationConfig
35
+
36
+ config = OscillationConfig(signal="respiration", smooth_window=100)
37
+ added = config.add_primitives(df, "subject")
38
+ summary = config.summarize(added, ["subject", config.trough_event_id_col])
39
+ ```
40
+
41
+ ## What's here
42
+
43
+ - `src/featuregraph_smoothing_core/` — the package
44
+ - `notebooks/bidmc_visual_demo.ipynb` — the code that produced Figure 1
45
+ - `artifacts/paper/compiler/smoothing.md` — the paper manuscript
46
+ - `tests/` — the full test suite
47
+
48
+ ## Citation
49
+
50
+ Software: https://doi.org/10.5281/zenodo.22947447
51
+
52
+ ## License
53
+
54
+ MIT. See `LICENSE`.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "featuregraph-smoothing-core"
7
+ version = "1.0.0"
8
+ description = "Minimal, frozen subset of FeatureGraph's state-detection code, extracted to support reproducibility of the smoothing-paper results."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Nazia Habib" },
14
+ ]
15
+ dependencies = [
16
+ "pandas>=1.5",
17
+ "numpy>=1.23",
18
+ "matplotlib>=3.5",
19
+ "requests>=2.25",
20
+ ]
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ from featuregraph_smoothing_core import datasets
2
+ from featuregraph_smoothing_core import validation
3
+ from featuregraph_smoothing_core.plot import (
4
+ plot,
5
+ plot_annotated_oscillation,
6
+ plot_oscillation_panel,
7
+ add_construction_legend,
8
+ )
9
+
10
+ __all__ = ["datasets", "validation", "plot", "plot_annotated_oscillation", "plot_oscillation_panel", "add_construction_legend"]
@@ -0,0 +1,101 @@
1
+ from dataclasses import dataclass
2
+ from featuregraph_smoothing_core.operators.states import rising_state, falling_state
3
+ from featuregraph_smoothing_core.operators.events import enter_state, exit_state, event_id, event_index
4
+ from featuregraph_smoothing_core.operators.measures import smooth, smooth_grouped
5
+
6
+ import pandas as pd
7
+ import numpy as np
8
+
9
+ @dataclass
10
+ class OscillationConfig:
11
+ signal: str
12
+ eps: float = 0
13
+ lag: int = 1
14
+ smooth_window: int = 100
15
+
16
+ @property
17
+ def smooth(self):
18
+ return f'{self.signal}_smooth'
19
+
20
+ @property
21
+ def rising_col(self):
22
+ return f'{self.signal}_rising'
23
+
24
+ @property
25
+ def falling_col(self):
26
+ return f'{self.signal}_falling'
27
+
28
+ @property
29
+ def enter_rising_col(self):
30
+ return f'enter_{self.signal}_rising'
31
+
32
+ @property
33
+ def exit_rising_col(self):
34
+ return f'exit_{self.signal}_rising'
35
+
36
+ @property
37
+ def peak_event_id_col(self):
38
+ return f'{self.signal}_peak_event_id'
39
+
40
+ @property
41
+ def trough_event_id_col(self):
42
+ return f'{self.signal}_trough_event_id'
43
+
44
+ @property
45
+ def peak_index_col(self):
46
+ return f'{self.signal}_peak_index'
47
+
48
+ @property
49
+ def trough_index_col(self):
50
+ return f'{self.signal}_trough_index'
51
+
52
+ def add_primitives(self, df: pd.DataFrame, group=None):
53
+ df = df.copy()
54
+ df[self.smooth] = smooth_grouped(df, self.signal, window=self.smooth_window, group=group)
55
+ df[self.rising_col] = rising_state(df[self.smooth])
56
+ df[self.falling_col] = falling_state(df[self.smooth])
57
+ df[self.exit_rising_col] = exit_state(df[self.rising_col], group=df[group] if group else None)
58
+ df[self.enter_rising_col] = enter_state(df[self.rising_col], group=df[group] if group else None)
59
+ df[self.peak_event_id_col] = event_id(df, self.exit_rising_col, group=group)
60
+ df[self.trough_event_id_col] = event_id(df, self.enter_rising_col, group=group)
61
+ df[self.peak_index_col] = event_index(df, self.exit_rising_col, group=group)
62
+ df[self.trough_index_col] = event_index(df, self.enter_rising_col, group=group)
63
+ return df
64
+
65
+ def summarize(self, df: pd.DataFrame, group=None):
66
+ summarydf = df.groupby(group).agg(
67
+ start_index=(self.trough_index_col, 'first'),
68
+ rising_duration=(self.rising_col, 'sum'),
69
+ falling_duration=(self.falling_col, 'sum'),
70
+ peak_index=(self.peak_index_col, 'last'),
71
+ trough_index=(self.trough_index_col, 'first'),
72
+ max_raw_signal=(self.signal, 'max'),
73
+ max_smooth_signal=(self.smooth, 'max'),
74
+ min_raw_signal=(self.signal, 'min'),
75
+ min_smooth_signal=(self.smooth, 'min'),
76
+ )
77
+
78
+ summarydf['end_index'] = summarydf.groupby(level=0)['start_index'].shift(-1)
79
+ summarydf['is_complete'] = summarydf['start_index'].notna() & summarydf['end_index'].notna()
80
+ summarydf['duration'] = summarydf['rising_duration'] + summarydf['falling_duration']
81
+
82
+ if not group:
83
+ summarydf['period'] = summarydf['peak_index'].diff()
84
+ else:
85
+ summarydf['period'] = summarydf.groupby(group[0])['peak_index'].diff()
86
+
87
+ net_change_raw = (summarydf['max_raw_signal'] - summarydf['min_raw_signal'])
88
+ net_change_smooth = (summarydf['max_smooth_signal'] - summarydf['min_smooth_signal'])
89
+
90
+ summarydf['amplitude_raw'] = net_change_raw / 2
91
+ summarydf['amplitude_smooth'] = net_change_smooth / 2
92
+
93
+ summarydf['raw_rising_mean_rate'] = (net_change_raw / summarydf['rising_duration']).where(summarydf['rising_duration'] > 0)
94
+ summarydf['raw_falling_mean_rate'] = (net_change_raw / summarydf['falling_duration']).where(summarydf['falling_duration'] > 0)
95
+
96
+ summarydf['smooth_rising_mean_rate'] = (net_change_smooth / summarydf['rising_duration']).where(summarydf['rising_duration'] > 0)
97
+ summarydf['smooth_falling_mean_rate'] = (net_change_smooth / summarydf['falling_duration']).where(summarydf['falling_duration'] > 0)
98
+
99
+ summarydf["temporal_symmetry"] = (1 - (summarydf['rising_duration'] - summarydf['falling_duration']).abs() / summarydf['duration']).where(summarydf['duration'] > 0)
100
+
101
+ return summarydf
@@ -0,0 +1,3 @@
1
+ from featuregraph_smoothing_core.datasets._bidmc import bidmc, bidmc_breaths
2
+
3
+ __all__ = ["bidmc", "bidmc_breaths"]
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import pandas as pd
4
+
5
+ from featuregraph_smoothing_core.utils._bidmc import (
6
+ load_bidmc_breaths,
7
+ load_bidmc_subject,
8
+ )
9
+ from featuregraph_smoothing_core.utils._rename_map import bidmc_map
10
+
11
+
12
+ def bidmc(
13
+ subject: int = 1,
14
+ *,
15
+ refresh: bool = False,
16
+ ) -> pd.DataFrame:
17
+ """
18
+ Load one BIDMC subject.
19
+
20
+ Returns
21
+ -------
22
+ pandas.DataFrame
23
+ Physiological waveform observations with standardized
24
+ FeatureGraph column names.
25
+ """
26
+ return (
27
+ load_bidmc_subject(
28
+ subject,
29
+ refresh=refresh,
30
+ )
31
+ .rename(columns=bidmc_map)
32
+ )
33
+
34
+
35
+ def bidmc_breaths(
36
+ subject: int = 1,
37
+ *,
38
+ refresh: bool = False,
39
+ ) -> pd.DataFrame:
40
+ """Load the two BIDMC breath-annotation columns for one subject."""
41
+ return load_bidmc_breaths(
42
+ subject,
43
+ refresh=refresh,
44
+ )
@@ -0,0 +1,27 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+ def enter_state(state, group=None):
5
+ x = state.astype(int)
6
+ if group is None:
7
+ return x.diff().eq(1)
8
+ return x.groupby(group).diff().eq(1)
9
+
10
+
11
+ def exit_state(state, group=None):
12
+ x = state.astype(int)
13
+ if group is None:
14
+ return x.diff().eq(-1)
15
+ return x.groupby(group).diff().eq(-1)
16
+
17
+
18
+ def event_id(df, enter_col, group=None):
19
+ if group is None:
20
+ return df[enter_col].cumsum()
21
+ return df.groupby(group)[enter_col].cumsum()
22
+
23
+ def event_index(df, event_col, group=None):
24
+ event_positions = pd.Series(np.where(df[event_col], df.index, np.nan), index=df.index)
25
+ if group is None:
26
+ return event_positions.ffill()
27
+ return event_positions.groupby(df[group]).ffill()
@@ -0,0 +1,13 @@
1
+ def smooth(signal, window):
2
+ return (
3
+ signal.rolling(center=True, window=window).median().rolling(center=True, window=window).mean()
4
+ )
5
+
6
+ def smooth_grouped(df, signal_col, window, group):
7
+ return df.groupby(group)[signal_col].transform(lambda s: smooth(s, window))
8
+
9
+ def group_transform(df, signal, op, group):
10
+ return df.groupby(group)[signal].transform(op)
11
+
12
+ def group_map(df, signal, op, group, offset=0):
13
+ return df[group].map(df.groupby(group)[signal].agg(op).shift(offset))
@@ -0,0 +1,30 @@
1
+ def positive_state(quantity, eps=0):
2
+ return quantity.gt(eps)
3
+
4
+
5
+ def negative_state(quantity, eps=0):
6
+ return quantity.lt(-eps)
7
+
8
+
9
+ def inactive_state(quantity, eps=0):
10
+ return quantity.abs().le(eps)
11
+
12
+
13
+ def rising_state(series, lag=1, eps=0):
14
+ return positive_state(series.diff(lag), eps)
15
+
16
+
17
+ def falling_state(series, lag=1, eps=0):
18
+ return negative_state(series.diff(lag), eps)
19
+
20
+
21
+ def stable_state(series, lag=1, eps=0):
22
+ return inactive_state(series.diff(lag), eps)
23
+
24
+
25
+ def accumulating_state(contribution, eps=0):
26
+ return positive_state(contribution, eps)
27
+
28
+
29
+ def depleting_state(contribution, eps=0):
30
+ return negative_state(contribution, eps)