tpcp 2.2.1__tar.gz → 2.3.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.
- {tpcp-2.2.1 → tpcp-2.3.0}/PKG-INFO +1 -1
- {tpcp-2.2.1 → tpcp-2.3.0}/pyproject.toml +23 -7
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/__init__.py +3 -2
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_dataset.py +92 -6
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_utils/_score.py +36 -38
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/caching.py +6 -4
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/misc/__init__.py +12 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/misc/_typed_iterator.py +54 -5
- tpcp-2.3.0/src/tpcp/misc/_warning_error_context.py +551 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/optimize/_optimize.py +106 -51
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/optimize/optuna.py +10 -3
- tpcp-2.3.0/src/tpcp/parallel.py +404 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/validate/_scorer.py +46 -37
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/validate/_validate.py +24 -25
- tpcp-2.2.1/src/tpcp/parallel.py +0 -156
- {tpcp-2.2.1 → tpcp-2.3.0}/README.md +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/skills/tpcp/tpcp-basics/SKILL.md +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/skills/tpcp/tpcp-builder/SKILL.md +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/skills/tpcp/tpcp-datasets/SKILL.md +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/skills/tpcp/tpcp-multiprocessing/SKILL.md +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/skills/tpcp/tpcp-optimization/SKILL.md +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_algorithm.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_algorithm_utils.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_base.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_cli.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_hash.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_optimize.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_parameters.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_pipeline.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_utils/__init__.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/_utils/_general.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/exceptions.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/misc/_class_utils.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/optimize/__init__.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/testing/__init__.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/testing/_algorithm_test_mixin.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/testing/_regression_utils.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/types.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/validate/__init__.py +0 -0
- {tpcp-2.2.1 → tpcp-2.3.0}/src/tpcp/validate/_cross_val_helper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: tpcp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Pipeline and Dataset helpers for complex algorithm evaluation.
|
|
5
5
|
Author: Arne Küderle, Robert Richer, Raul C. Sîmpetru, Björn Eskofier
|
|
6
6
|
Author-email: Arne Küderle <arne.kuederle@fau.de>, Robert Richer <robert.richer@fau.de>, Raul C. Sîmpetru <raul.simpetru@fau.de>, Björn Eskofier <bjoern.eskofier@fau.de>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tpcp"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.0"
|
|
4
4
|
description = "Pipeline and Dataset helpers for complex algorithm evaluation."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Arne Küderle", email = "arne.kuederle@fau.de" },
|
|
@@ -36,11 +36,22 @@ tpcp = "tpcp._cli:main"
|
|
|
36
36
|
tpcp_snapshots = "tpcp.testing._regression_utils"
|
|
37
37
|
|
|
38
38
|
[dependency-groups]
|
|
39
|
-
|
|
40
|
-
"
|
|
39
|
+
test = [
|
|
40
|
+
"attrs>=22.1.0",
|
|
41
|
+
"matplotlib>=3.4.3,<4",
|
|
42
|
+
"numpydoc>=1.4.0,<2",
|
|
43
|
+
"optuna>=2.10",
|
|
41
44
|
"pytest>=8.4.2,<9",
|
|
45
|
+
"pytest-xdist>=3.8.0,<4",
|
|
46
|
+
]
|
|
47
|
+
coverage = [
|
|
42
48
|
"pytest-cov>=7.0.0,<8",
|
|
43
|
-
|
|
49
|
+
]
|
|
50
|
+
lint = [
|
|
51
|
+
"ruff>=0.14.0,<0.15",
|
|
52
|
+
]
|
|
53
|
+
docs = [
|
|
54
|
+
"pydata-sphinx-theme>=0.14.1,<0.15",
|
|
44
55
|
"pyright>=1.1.230,<2",
|
|
45
56
|
"numpydoc>=1.4.0,<2",
|
|
46
57
|
"sphinx-gallery>=0.14.0,<0.15",
|
|
@@ -49,8 +60,13 @@ dev = [
|
|
|
49
60
|
"matplotlib>=3.4.3,<4",
|
|
50
61
|
"toml>=0.10.2,<0.11",
|
|
51
62
|
"Sphinx>=7.2.6,<8",
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
]
|
|
64
|
+
dev = [
|
|
65
|
+
{ include-group = "test" },
|
|
66
|
+
{ include-group = "coverage" },
|
|
67
|
+
{ include-group = "lint" },
|
|
68
|
+
{ include-group = "docs" },
|
|
69
|
+
"poethepoet>=0.10.0,<0.11",
|
|
54
70
|
]
|
|
55
71
|
|
|
56
72
|
[tool.uv]
|
|
@@ -82,7 +98,7 @@ lint = { cmd = "ruff check src/tpcp --fix", help = "Lint all files with ruff." }
|
|
|
82
98
|
_lint_ci = "ruff check src/tpcp --output-format=github"
|
|
83
99
|
_check_format = "ruff format . --check"
|
|
84
100
|
ci_check = { sequence = ["_check_format", "_lint_ci"], help = "Check all potential format and linting issues." }
|
|
85
|
-
test = { cmd = "pytest ./tests --cov=tpcp --cov-report=term-missing --cov-report=xml", help = "Run Pytest with coverage." }
|
|
101
|
+
test = { cmd = "pytest ./tests -n 2 --dist=worksteal --cov=tpcp --cov-report=term-missing --cov-report=xml", help = "Run Pytest with coverage." }
|
|
86
102
|
docs = { "script" = "_tasks:task_docs()", help = "Build the html docs using Sphinx." }
|
|
87
103
|
docs_clean = { "script" = "_tasks:task_docs(clean=True)", help = "Remove all old build files and build a clean version of the docs." }
|
|
88
104
|
docs_preview = { cmd = "python -m http.server --directory docs/_build/html", help = "Preview the built html docs." }
|
|
@@ -11,7 +11,7 @@ from tpcp._algorithm_utils import (
|
|
|
11
11
|
make_optimize_safe,
|
|
12
12
|
)
|
|
13
13
|
from tpcp._base import NOTHING, BaseFactory, BaseTpcpObject, CloneFactory, cf, clone, get_param_names
|
|
14
|
-
from tpcp._dataset import Dataset
|
|
14
|
+
from tpcp._dataset import Dataset, DatasetWrapperMixin
|
|
15
15
|
from tpcp._parameters import (
|
|
16
16
|
HyperPara,
|
|
17
17
|
HyperParameter,
|
|
@@ -24,7 +24,7 @@ from tpcp._parameters import (
|
|
|
24
24
|
)
|
|
25
25
|
from tpcp._pipeline import OptimizablePipeline, Pipeline
|
|
26
26
|
|
|
27
|
-
__version__ = "2.
|
|
27
|
+
__version__ = "2.3.0"
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
__all__ = [
|
|
@@ -34,6 +34,7 @@ __all__ = [
|
|
|
34
34
|
"BaseTpcpObject",
|
|
35
35
|
"CloneFactory",
|
|
36
36
|
"Dataset",
|
|
37
|
+
"DatasetWrapperMixin",
|
|
37
38
|
"HyperPara",
|
|
38
39
|
"HyperParameter",
|
|
39
40
|
"OptiPara",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import warnings
|
|
4
4
|
from collections.abc import Iterator, Sequence
|
|
5
5
|
from keyword import iskeyword
|
|
6
|
-
from typing import Generic, Optional, TypeVar, Union, cast, get_args, overload
|
|
6
|
+
from typing import ClassVar, Generic, Optional, TypeVar, Union, cast, get_args, get_origin, overload
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pandas as pd
|
|
@@ -125,7 +125,7 @@ class _Dataset(BaseTpcpObject, Generic[GroupLabelT]):
|
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
# Get the generic type of the dataset
|
|
128
|
-
group_label_type =
|
|
128
|
+
group_label_type = self._get_group_label_type()
|
|
129
129
|
# If group label type is a named tuple, we check that the keys are the same as the index columns
|
|
130
130
|
if (label_fields := getattr(group_label_type, "_fields", None)) and label_fields != (
|
|
131
131
|
index_cols := tuple(index_1.columns)
|
|
@@ -155,7 +155,7 @@ class _Dataset(BaseTpcpObject, Generic[GroupLabelT]):
|
|
|
155
155
|
|
|
156
156
|
For some examples and additional explanation see this :ref:`example <custom_dataset_basics>`.
|
|
157
157
|
"""
|
|
158
|
-
if getattr(group_label_type :=
|
|
158
|
+
if getattr(group_label_type := self._get_group_label_type(), "_fields", None):
|
|
159
159
|
nd_tuple_name = group_label_type.__name__
|
|
160
160
|
else:
|
|
161
161
|
nd_tuple_name = type(self).__name__ + "GroupLabel"
|
|
@@ -198,7 +198,7 @@ class _Dataset(BaseTpcpObject, Generic[GroupLabelT]):
|
|
|
198
198
|
|
|
199
199
|
def index_as_tuples(self) -> list[GroupLabelT]:
|
|
200
200
|
"""Get all datapoint labels of the dataset (i.e. a list of the rows of the index as named tuples)."""
|
|
201
|
-
if getattr(group_label_type :=
|
|
201
|
+
if getattr(group_label_type := self._get_group_label_type(), "_fields", None):
|
|
202
202
|
# If a generic is provided, we actually convert the named tuples.
|
|
203
203
|
return [group_label_type(*row) for row in self.index.itertuples(index=False)]
|
|
204
204
|
return list(self.index.itertuples(index=False, name=type(self).__name__ + "GroupLabel"))
|
|
@@ -234,11 +234,22 @@ class _Dataset(BaseTpcpObject, Generic[GroupLabelT]):
|
|
|
234
234
|
) from e
|
|
235
235
|
|
|
236
236
|
def _get_groupby_columns(self) -> list[str]:
|
|
237
|
-
"""Get the groupby columns."""
|
|
237
|
+
"""Get the groupby columns after materializing the dataset index."""
|
|
238
|
+
# `create_index` may establish the grouping based on the materialized index.
|
|
239
|
+
index = self.index
|
|
238
240
|
if self.groupby_cols is None:
|
|
239
|
-
return
|
|
241
|
+
return index.columns.to_list()
|
|
240
242
|
return _ensure_is_list(self.groupby_cols)
|
|
241
243
|
|
|
244
|
+
def _get_group_label_type(self):
|
|
245
|
+
"""Find the concrete group-label type in the dataset inheritance hierarchy."""
|
|
246
|
+
for cls in type(self).__mro__:
|
|
247
|
+
for generic_base in getattr(cls, "__orig_bases__", ()):
|
|
248
|
+
origin = get_origin(generic_base)
|
|
249
|
+
if isinstance(origin, type) and issubclass(origin, _Dataset) and (args := get_args(generic_base)):
|
|
250
|
+
return args[0]
|
|
251
|
+
return None
|
|
252
|
+
|
|
242
253
|
def _get_unique_groups(self) -> Union[pd.MultiIndex, pd.Index]:
|
|
243
254
|
return self.grouped_index.index.unique()
|
|
244
255
|
|
|
@@ -750,6 +761,81 @@ class Dataset(_Dataset[GroupLabelT], Generic[GroupLabelT]):
|
|
|
750
761
|
return DatasetAt
|
|
751
762
|
|
|
752
763
|
|
|
764
|
+
WrappedDatasetT = TypeVar("WrappedDatasetT", bound="_Dataset")
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class DatasetWrapperMixin(Generic[WrappedDatasetT]):
|
|
768
|
+
"""Provide common behavior for datasets that wrap another dataset.
|
|
769
|
+
|
|
770
|
+
This mixin expands the index of ``wrapped_dataset`` through
|
|
771
|
+
:meth:`_create_wrapped_index`, links the wrapper's initial grouping to the
|
|
772
|
+
wrapped dataset, and resolves the source datapoint represented by a wrapper
|
|
773
|
+
datapoint through :attr:`wrapped_datapoint`.
|
|
774
|
+
|
|
775
|
+
The mixin deliberately does not inherit from :class:`Dataset`. A concrete
|
|
776
|
+
wrapper must inherit from both this mixin and the domain-specific dataset
|
|
777
|
+
interface implemented by the wrapped dataset.
|
|
778
|
+
|
|
779
|
+
.. important::
|
|
780
|
+
``DatasetWrapperMixin`` must be listed **before** the dataset base class.
|
|
781
|
+
The order controls Python's method resolution order and ensures that this
|
|
782
|
+
mixin's :meth:`create_index` implementation is used. Reversing the bases
|
|
783
|
+
raises a :class:`TypeError` when the wrapper class is defined.
|
|
784
|
+
|
|
785
|
+
Examples
|
|
786
|
+
--------
|
|
787
|
+
The mixin comes first and the shared dataset interface comes second::
|
|
788
|
+
|
|
789
|
+
class AugmentedDataset(
|
|
790
|
+
DatasetWrapperMixin[SourceDataset],
|
|
791
|
+
SourceDataset,
|
|
792
|
+
):
|
|
793
|
+
_wrapper_groupby_cols = ("augmentation",)
|
|
794
|
+
|
|
795
|
+
def _create_wrapped_index(self, source_index): ...
|
|
796
|
+
|
|
797
|
+
"""
|
|
798
|
+
|
|
799
|
+
wrapped_dataset: WrappedDatasetT
|
|
800
|
+
_wrapper_groupby_cols: ClassVar[tuple[str, ...]]
|
|
801
|
+
|
|
802
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
803
|
+
"""Validate that the mixin precedes the dataset base in the MRO."""
|
|
804
|
+
super().__init_subclass__(**kwargs)
|
|
805
|
+
mro = cls.__mro__
|
|
806
|
+
if _Dataset in mro and mro.index(DatasetWrapperMixin) > mro.index(_Dataset):
|
|
807
|
+
raise TypeError(
|
|
808
|
+
"DatasetWrapperMixin must be listed before the Dataset base class so its create_index method takes "
|
|
809
|
+
"precedence in the method resolution order."
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
def create_index(self) -> pd.DataFrame:
|
|
813
|
+
"""Create the wrapper index and link its initial grouping to the wrapped dataset."""
|
|
814
|
+
source_index = self.wrapped_dataset.index.copy()
|
|
815
|
+
dataset = cast("_Dataset", self)
|
|
816
|
+
|
|
817
|
+
if dataset.groupby_cols is None:
|
|
818
|
+
dataset.groupby_cols = [*self.wrapped_dataset._get_groupby_columns(), *self._wrapper_groupby_cols]
|
|
819
|
+
|
|
820
|
+
return self._create_wrapped_index(source_index)
|
|
821
|
+
|
|
822
|
+
def _create_wrapped_index(self, source_index: pd.DataFrame) -> pd.DataFrame:
|
|
823
|
+
"""Create the wrapper index from a copy of the wrapped dataset index."""
|
|
824
|
+
raise NotImplementedError
|
|
825
|
+
|
|
826
|
+
@property
|
|
827
|
+
def wrapped_datapoint(self) -> WrappedDatasetT:
|
|
828
|
+
"""Return the wrapped datapoint represented by the current wrapper group."""
|
|
829
|
+
dataset = cast("_Dataset", self)
|
|
830
|
+
dataset.assert_is_single_group("wrapped_datapoint")
|
|
831
|
+
|
|
832
|
+
source_columns = self.wrapped_dataset.index.columns.to_list()
|
|
833
|
+
source_index = pd.DataFrame(dataset.index.loc[:, source_columns]).drop_duplicates().reset_index(drop=True)
|
|
834
|
+
source_groupby = [column for column in dataset._get_groupby_columns() if column in source_columns]
|
|
835
|
+
|
|
836
|
+
return self.wrapped_dataset.get_subset(index=source_index).groupby(source_groupby or None)
|
|
837
|
+
|
|
838
|
+
|
|
753
839
|
T = TypeVar("T")
|
|
754
840
|
|
|
755
841
|
|
|
@@ -17,6 +17,7 @@ from tpcp._base import clone
|
|
|
17
17
|
from tpcp._hash import custom_hash
|
|
18
18
|
from tpcp._utils._general import _get_nested_paras
|
|
19
19
|
from tpcp.exceptions import OptimizationError, TestError
|
|
20
|
+
from tpcp.misc import warning_error_context
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
22
23
|
from tpcp._dataset import Dataset
|
|
@@ -62,7 +63,6 @@ def _score(
|
|
|
62
63
|
return_parameters=False,
|
|
63
64
|
return_data_labels=False,
|
|
64
65
|
return_times=False,
|
|
65
|
-
error_info: Optional[str] = None,
|
|
66
66
|
) -> _ScoreResults:
|
|
67
67
|
"""Set parameters and return score.
|
|
68
68
|
|
|
@@ -104,15 +104,15 @@ def _score(
|
|
|
104
104
|
|
|
105
105
|
pipeline = pipeline.set_params(**parameters)
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
score_context: dict[str, Any] = {}
|
|
108
|
+
with warning_error_context("score", context_provider=lambda: score_context):
|
|
109
|
+
try:
|
|
110
|
+
start_time = time.time()
|
|
111
|
+
score_context = {"data_labels": dataset.group_labels}
|
|
112
|
+
agg_scores, single_scores = scorer(pipeline, dataset)
|
|
113
|
+
score_time = time.time() - start_time
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise TestError("Testing failed.") from e
|
|
116
116
|
|
|
117
117
|
result: _ScoreResults = {
|
|
118
118
|
"scores": agg_scores,
|
|
@@ -142,7 +142,6 @@ def _optimize_and_score(
|
|
|
142
142
|
return_data_labels=False,
|
|
143
143
|
return_times=False,
|
|
144
144
|
memory: Optional[Memory] = None,
|
|
145
|
-
error_info: Optional[str] = None,
|
|
146
145
|
) -> _OptimizeScoreResults:
|
|
147
146
|
"""Optimize and score the optimized pipeline on the train and test data, respectively.
|
|
148
147
|
|
|
@@ -177,17 +176,17 @@ def _optimize_and_score(
|
|
|
177
176
|
|
|
178
177
|
optimize_params_clean: dict = optimize_params or {}
|
|
179
178
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
179
|
+
optimize_context: dict[str, Any] = {}
|
|
180
|
+
with warning_error_context("optimize", context_provider=lambda: optimize_context):
|
|
181
|
+
try:
|
|
182
|
+
start_time = time.time()
|
|
183
|
+
optimize_context = {"data_labels": train_set.group_labels}
|
|
184
|
+
optimizer = _cached_optimize(
|
|
185
|
+
optimizer, train_set, hyperparameters, pure_parameters, memory, optimize_params_clean
|
|
186
|
+
)
|
|
187
|
+
optimize_time = time.time() - start_time
|
|
188
|
+
except Exception as e:
|
|
189
|
+
raise OptimizationError("Optimization failed.") from e
|
|
191
190
|
|
|
192
191
|
# Now we set the remaining paras.
|
|
193
192
|
# Because, we need to set the parameters on the optimized pipeline and not the input pipeline we strip the
|
|
@@ -198,28 +197,27 @@ def _optimize_and_score(
|
|
|
198
197
|
# beginning.
|
|
199
198
|
optimizer = optimizer.set_params(**pure_parameters)
|
|
200
199
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
200
|
+
test_score_context: dict[str, Any] = {}
|
|
201
|
+
with warning_error_context("test_score", context_provider=lambda: test_score_context):
|
|
202
|
+
try:
|
|
203
|
+
test_score_context = {"data_labels": test_set.group_labels}
|
|
204
|
+
agg_scores, single_scores = scorer(optimizer.optimized_pipeline_, test_set)
|
|
205
|
+
score_time = time.time() - optimize_time - start_time
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise TestError("Testing failed.") from e
|
|
209
208
|
|
|
210
209
|
result: _OptimizeScoreResults = {
|
|
211
210
|
"test__scores": agg_scores,
|
|
212
211
|
"test__single__scores": single_scores,
|
|
213
212
|
}
|
|
214
213
|
if return_train_score:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
) from e
|
|
214
|
+
train_score_context: dict[str, Any] = {}
|
|
215
|
+
with warning_error_context("train_score", context_provider=lambda: train_score_context):
|
|
216
|
+
try:
|
|
217
|
+
train_score_context = {"data_labels": train_set.group_labels}
|
|
218
|
+
train_agg_scores, train_single_scores = scorer(optimizer.optimized_pipeline_, train_set)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
raise TestError("Testing failed.") from e
|
|
223
221
|
result["train__scores"] = train_agg_scores
|
|
224
222
|
result["train__single__scores"] = train_single_scores
|
|
225
223
|
if return_times:
|
|
@@ -14,7 +14,7 @@ from joblib import Memory
|
|
|
14
14
|
|
|
15
15
|
from tpcp import Algorithm, get_action_methods_names, get_results, make_action_safe
|
|
16
16
|
from tpcp._hash import custom_hash
|
|
17
|
-
from tpcp.parallel import
|
|
17
|
+
from tpcp.parallel import _register_tpcp_global_parallel_callback, _remove_tpcp_global_parallel_callback
|
|
18
18
|
|
|
19
19
|
_ALREADY_WARNED = False
|
|
20
20
|
|
|
@@ -130,7 +130,7 @@ def _register_global_parallel_callback(func, name):
|
|
|
130
130
|
def _callback():
|
|
131
131
|
return None, wrapper
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
_register_tpcp_global_parallel_callback(name, _callback)
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
def global_disk_cache( # noqa: C901
|
|
@@ -313,7 +313,9 @@ def remove_disk_cache(algorithm_object: type[Algorithm]):
|
|
|
313
313
|
if getattr(action_method, "__wrapped__", None) is not None:
|
|
314
314
|
setattr(algorithm_object, action_name, action_method.__wrapped__)
|
|
315
315
|
with contextlib.suppress(KeyError):
|
|
316
|
-
|
|
316
|
+
_remove_tpcp_global_parallel_callback(
|
|
317
|
+
f"global_disk_cache__{algorithm_object.__qualname__}__{action_name}"
|
|
318
|
+
)
|
|
317
319
|
return algorithm_object
|
|
318
320
|
|
|
319
321
|
|
|
@@ -453,7 +455,7 @@ def remove_ram_cache(algorithm_object: type[Algorithm]):
|
|
|
453
455
|
if getattr(action_method, "__wrapped__", None) is not None:
|
|
454
456
|
setattr(algorithm_object, action_method_name, action_method.__wrapped__)
|
|
455
457
|
with contextlib.suppress(KeyError):
|
|
456
|
-
|
|
458
|
+
_remove_tpcp_global_parallel_callback(
|
|
457
459
|
f"global_ram_cache__{algorithm_object.__qualname__}__{action_method_name}"
|
|
458
460
|
)
|
|
459
461
|
|
|
@@ -3,12 +3,24 @@
|
|
|
3
3
|
from tpcp._hash import custom_hash
|
|
4
4
|
from tpcp.misc._class_utils import classproperty, set_defaults
|
|
5
5
|
from tpcp.misc._typed_iterator import BaseTypedIterator, TypedIterator, TypedIteratorResultTuple
|
|
6
|
+
from tpcp.misc._warning_error_context import (
|
|
7
|
+
WarningErrorContext,
|
|
8
|
+
WarningErrorContextRecord,
|
|
9
|
+
iter_with_warning_error_context,
|
|
10
|
+
print_with_context,
|
|
11
|
+
warning_error_context,
|
|
12
|
+
)
|
|
6
13
|
|
|
7
14
|
__all__ = [
|
|
8
15
|
"BaseTypedIterator",
|
|
9
16
|
"TypedIterator",
|
|
10
17
|
"TypedIteratorResultTuple",
|
|
18
|
+
"WarningErrorContext",
|
|
19
|
+
"WarningErrorContextRecord",
|
|
11
20
|
"classproperty",
|
|
12
21
|
"custom_hash",
|
|
22
|
+
"iter_with_warning_error_context",
|
|
23
|
+
"print_with_context",
|
|
13
24
|
"set_defaults",
|
|
25
|
+
"warning_error_context",
|
|
14
26
|
]
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import warnings
|
|
2
|
-
from collections.abc import Iterable, Iterator, Sequence
|
|
2
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
|
3
3
|
from dataclasses import fields, is_dataclass
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
5
5
|
|
|
6
6
|
from typing_extensions import NamedTuple, TypeAlias
|
|
7
7
|
|
|
8
8
|
from tpcp import Algorithm, cf
|
|
9
|
+
from tpcp.misc._warning_error_context import WarningErrorContext
|
|
10
|
+
from tpcp.misc._warning_error_context import warning_error_context as _warning_error_context
|
|
9
11
|
|
|
10
12
|
DataclassT = TypeVar("DataclassT")
|
|
11
13
|
InputTypeT = TypeVar("InputTypeT")
|
|
@@ -26,6 +28,7 @@ class _NotSet:
|
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
_NULL_VALUE = _NotSet()
|
|
31
|
+
_NO_PREVIOUS_ITERATION_INDEX = object()
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class BaseTypedIterator(Algorithm, Generic[InputTypeT, DataclassT]):
|
|
@@ -159,6 +162,10 @@ class BaseTypedIterator(Algorithm, Generic[InputTypeT, DataclassT]):
|
|
|
159
162
|
The result object is a dataclass instance of the type defined in ``self.data_type``.
|
|
160
163
|
All values of the result object are set to ``TypedIterator.NULL_VALUE`` by default.
|
|
161
164
|
|
|
165
|
+
To add context to warnings and exceptions from the loop body, explicitly enter
|
|
166
|
+
:meth:`warning_error_context` inside the loop. Context must not remain active across this generator's
|
|
167
|
+
``yield`` boundary.
|
|
168
|
+
|
|
162
169
|
"""
|
|
163
170
|
if not is_dataclass(self.data_type):
|
|
164
171
|
raise TypeError(f"Expected a dataclass as data_type, got {self.data_type}")
|
|
@@ -173,13 +180,55 @@ class BaseTypedIterator(Algorithm, Generic[InputTypeT, DataclassT]):
|
|
|
173
180
|
self._raw_results = []
|
|
174
181
|
|
|
175
182
|
self.done_[iteration_name] = False
|
|
176
|
-
for d in iterable:
|
|
183
|
+
for i, d in enumerate(iterable):
|
|
177
184
|
result_object = self._get_new_empty_object()
|
|
178
|
-
|
|
185
|
+
iteration_context = iteration_context or {}
|
|
186
|
+
result_tuple = TypedIteratorResultTuple(iteration_name, d, result_object, iteration_context)
|
|
179
187
|
self._report_new_result(result_tuple)
|
|
180
|
-
|
|
188
|
+
previous_i = getattr(self, "_warning_error_context_i", _NO_PREVIOUS_ITERATION_INDEX)
|
|
189
|
+
self._warning_error_context_i = i
|
|
190
|
+
try:
|
|
191
|
+
yield d, result_object
|
|
192
|
+
finally:
|
|
193
|
+
if previous_i is _NO_PREVIOUS_ITERATION_INDEX:
|
|
194
|
+
del self._warning_error_context_i
|
|
195
|
+
else:
|
|
196
|
+
self._warning_error_context_i = previous_i
|
|
181
197
|
self.done_[iteration_name] = True
|
|
182
198
|
|
|
199
|
+
def warning_error_context(
|
|
200
|
+
self,
|
|
201
|
+
name: str,
|
|
202
|
+
context: Optional[dict[str, Any]] = None,
|
|
203
|
+
/,
|
|
204
|
+
*,
|
|
205
|
+
context_provider: Optional[Callable[[], Mapping[str, Any]]] = None,
|
|
206
|
+
record_only: bool = False,
|
|
207
|
+
) -> WarningErrorContext:
|
|
208
|
+
"""Create explicit warning/error context for an iteration body.
|
|
209
|
+
|
|
210
|
+
This convenience method has the same interface as
|
|
211
|
+
:func:`~tpcp.misc.warning_error_context` and adds the current zero-based
|
|
212
|
+
iteration index as ``i``. It deliberately does not infer any other context
|
|
213
|
+
from the iterator or its current item. Enter the returned context manager
|
|
214
|
+
inside the iteration body and pass all other relevant context explicitly.
|
|
215
|
+
"""
|
|
216
|
+
context = {} if context is None else context
|
|
217
|
+
if "i" in context:
|
|
218
|
+
raise ValueError("The context key 'i' is reserved by TypedIterator.")
|
|
219
|
+
try:
|
|
220
|
+
i = self._warning_error_context_i
|
|
221
|
+
except AttributeError as exc:
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
"warning_error_context must be called inside an active TypedIterator loop body."
|
|
224
|
+
) from exc
|
|
225
|
+
return _warning_error_context(
|
|
226
|
+
name,
|
|
227
|
+
{"i": i, **context},
|
|
228
|
+
context_provider=context_provider,
|
|
229
|
+
record_only=record_only,
|
|
230
|
+
)
|
|
231
|
+
|
|
183
232
|
def _get_new_empty_object(self) -> DataclassT:
|
|
184
233
|
init_dict = {k.name: self.NULL_VALUE for k in fields(self.data_type)}
|
|
185
234
|
return self.data_type(**init_dict)
|