xplainable-preprocessing 0.2.1__tar.gz → 0.2.3__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.
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/PKG-INFO +1 -1
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/pyproject.toml +1 -1
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/compiler.py +33 -4
- xplainable_preprocessing-0.2.3/src/xplainable_preprocessing/transformers/groupby_agg.py +143 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_compiler.py +82 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_transformers/test_all_transformers.py +126 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/.github/workflows/publish-pypi.yml +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/.gitignore +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/README.md +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/docs/dag-pipeline-proposal.md +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/docs/feature-pipeline-architectures.md +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/docs/feature-store-proposal.md +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/__init__.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/pipeline.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/preview.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/registry.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/sandbox.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/schema.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/serialization.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/__init__.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/category_condense.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/clip.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/datetime_extract.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/drop_columns.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/expression.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/fill_missing.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/grouped_lag.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/missing_flag.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/rename_columns.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/rolling_agg.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/text_clean.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/src/xplainable_preprocessing/transformers/type_cast.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/__init__.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_preview.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_sandbox.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_schema.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_serialization.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_transformers/__init__.py +0 -0
- {xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_transformers/test_expression.py +0 -0
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
6
9
|
|
|
7
10
|
from xplainable_preprocessing.pipeline import DataFrameColumnTransformer, DataFramePipeline
|
|
8
11
|
from xplainable_preprocessing.registry import REGISTRY
|
|
@@ -11,14 +14,29 @@ from xplainable_preprocessing.schema import PipelineSpec
|
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
def _coerce_params(cls, params: dict) -> dict:
|
|
14
|
-
"""
|
|
17
|
+
"""Adapt spec params to the constructor's signature.
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
- Converts list params to tuples where the constructor expects them
|
|
20
|
+
(JSON has no tuple type, so specs always contain lists; sklearn
|
|
21
|
+
transformers like MinMaxScaler expect tuples for feature_range).
|
|
22
|
+
- Drops params the constructor does not accept, with a warning. Specs
|
|
23
|
+
may be generated in an env with a newer sklearn than the one
|
|
24
|
+
compiling them (2026-07: sklearn 1.7 emitted
|
|
25
|
+
KBinsDiscretizer(quantile_method=...), which 1.6 rejects).
|
|
18
26
|
"""
|
|
19
27
|
sig = inspect.signature(cls.__init__)
|
|
28
|
+
accepts_kwargs = any(
|
|
29
|
+
p.kind is inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
30
|
+
)
|
|
20
31
|
coerced = {}
|
|
21
32
|
for key, value in params.items():
|
|
33
|
+
if key not in sig.parameters and not accepts_kwargs:
|
|
34
|
+
logger.warning(
|
|
35
|
+
"%s does not accept param '%s' in this environment "
|
|
36
|
+
"(likely a library-version mismatch with the spec's "
|
|
37
|
+
"generator); dropping it", cls.__name__, key
|
|
38
|
+
)
|
|
39
|
+
continue
|
|
22
40
|
if isinstance(value, list) and key in sig.parameters:
|
|
23
41
|
param = sig.parameters[key]
|
|
24
42
|
if param.default is not inspect.Parameter.empty and isinstance(param.default, tuple):
|
|
@@ -48,6 +66,8 @@ def compile_spec(spec: PipelineSpec) -> DataFramePipeline:
|
|
|
48
66
|
steps = []
|
|
49
67
|
|
|
50
68
|
for step in spec.steps:
|
|
69
|
+
wrap = bool(step.columns)
|
|
70
|
+
|
|
51
71
|
if step.type == "custom":
|
|
52
72
|
transformer = compile_custom(step.params)
|
|
53
73
|
else:
|
|
@@ -58,9 +78,18 @@ def compile_spec(spec: PipelineSpec) -> DataFramePipeline:
|
|
|
58
78
|
)
|
|
59
79
|
cls = REGISTRY[step.type]
|
|
60
80
|
params = _coerce_params(cls, step.params)
|
|
81
|
+
|
|
82
|
+
# Column-aware transformers (constructor takes `columns`) must
|
|
83
|
+
# receive the step-level columns directly and must NOT be wrapped:
|
|
84
|
+
# DataFrameColumnTransformer feeds them only their declared columns
|
|
85
|
+
# and re-attaches the result, which turns e.g. a drop into a no-op.
|
|
86
|
+
if step.columns and "columns" in inspect.signature(cls.__init__).parameters:
|
|
87
|
+
params.setdefault("columns", step.columns)
|
|
88
|
+
wrap = False
|
|
89
|
+
|
|
61
90
|
transformer = cls(**params)
|
|
62
91
|
|
|
63
|
-
if
|
|
92
|
+
if wrap:
|
|
64
93
|
transformer = DataFrameColumnTransformer(transformer, step.columns)
|
|
65
94
|
|
|
66
95
|
steps.append((step.id, transformer))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""GroupBy aggregation transformer for collapsing rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GroupByAggTransformer(BaseEstimator, TransformerMixin):
|
|
10
|
+
"""Aggregate rows by grouping columns, collapsing many rows into one per group.
|
|
11
|
+
|
|
12
|
+
Supports standard pandas aggregation functions plus ``mode`` and ``nunique``.
|
|
13
|
+
Each aggregation entry maps a source column to one or more operations, with
|
|
14
|
+
optional output column renaming.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
group_by : list[str]
|
|
19
|
+
Columns to group by. These become the index of the result and are
|
|
20
|
+
always retained in the output.
|
|
21
|
+
aggregations : dict
|
|
22
|
+
Mapping of ``source_column`` to aggregation specification(s).
|
|
23
|
+
|
|
24
|
+
Each value can be:
|
|
25
|
+
|
|
26
|
+
- A string: a single function name (e.g. ``"sum"``, ``"mean"``).
|
|
27
|
+
The output column keeps the source name.
|
|
28
|
+
- A dict with ``func`` and optional ``output``:
|
|
29
|
+
``{"func": "sum", "output": "total_revenue"}``.
|
|
30
|
+
- A list of the above, producing multiple output columns from one
|
|
31
|
+
source.
|
|
32
|
+
|
|
33
|
+
Supported functions: ``sum``, ``mean``, ``median``, ``min``, ``max``,
|
|
34
|
+
``std``, ``count``, ``first``, ``last``, ``nunique``, ``mode``.
|
|
35
|
+
|
|
36
|
+
Examples
|
|
37
|
+
--------
|
|
38
|
+
>>> spec_params = {
|
|
39
|
+
... "group_by": ["customer_id"],
|
|
40
|
+
... "aggregations": {
|
|
41
|
+
... "country": "first",
|
|
42
|
+
... "order_id": {"func": "count", "output": "orders_count"},
|
|
43
|
+
... "revenue": [
|
|
44
|
+
... {"func": "sum", "output": "total_spent"},
|
|
45
|
+
... {"func": "mean", "output": "avg_order_value"},
|
|
46
|
+
... ],
|
|
47
|
+
... "product_id": {"func": "nunique", "output": "unique_products"},
|
|
48
|
+
... "category": {"func": "mode", "output": "primary_category"},
|
|
49
|
+
... },
|
|
50
|
+
... }
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
FUNCTIONS = {
|
|
54
|
+
"sum", "mean", "median", "min", "max", "std",
|
|
55
|
+
"count", "first", "last", "nunique", "mode",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
group_by: list[str] | None = None,
|
|
61
|
+
aggregations: dict | None = None,
|
|
62
|
+
):
|
|
63
|
+
self.group_by = group_by or []
|
|
64
|
+
self.aggregations = aggregations or {}
|
|
65
|
+
|
|
66
|
+
def fit(self, X: pd.DataFrame, y=None):
|
|
67
|
+
# Validate all requested functions
|
|
68
|
+
for col, specs in self._iter_specs():
|
|
69
|
+
for func, _ in specs:
|
|
70
|
+
if func not in self.FUNCTIONS:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Column '{col}': unknown function '{func}'. "
|
|
73
|
+
f"Available: {sorted(self.FUNCTIONS)}"
|
|
74
|
+
)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
|
|
78
|
+
if not self.group_by:
|
|
79
|
+
raise ValueError("group_by must be specified")
|
|
80
|
+
if not self.aggregations:
|
|
81
|
+
raise ValueError("aggregations must be specified")
|
|
82
|
+
|
|
83
|
+
grouped = X.groupby(self.group_by, sort=False)
|
|
84
|
+
|
|
85
|
+
result_parts: dict[str, pd.Series] = {}
|
|
86
|
+
|
|
87
|
+
for col, specs in self._iter_specs():
|
|
88
|
+
if col not in X.columns:
|
|
89
|
+
continue
|
|
90
|
+
for func, output_name in specs:
|
|
91
|
+
result_parts[output_name] = self._apply_func(grouped, col, func)
|
|
92
|
+
|
|
93
|
+
result = pd.DataFrame(result_parts)
|
|
94
|
+
|
|
95
|
+
# Restore group_by columns from the index
|
|
96
|
+
for i, col in enumerate(self.group_by):
|
|
97
|
+
if len(self.group_by) == 1:
|
|
98
|
+
result.insert(i, col, result.index)
|
|
99
|
+
else:
|
|
100
|
+
result.insert(i, col, result.index.get_level_values(i))
|
|
101
|
+
|
|
102
|
+
return result.reset_index(drop=True)
|
|
103
|
+
|
|
104
|
+
def _iter_specs(self):
|
|
105
|
+
"""Yield (source_column, [(func, output_name), ...]) tuples."""
|
|
106
|
+
for col, spec in self.aggregations.items():
|
|
107
|
+
yield col, self._normalise(col, spec)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _normalise(col: str, spec) -> list[tuple[str, str]]:
|
|
111
|
+
"""Normalise the various spec formats into [(func, output), ...]."""
|
|
112
|
+
if isinstance(spec, str):
|
|
113
|
+
return [(spec, col)]
|
|
114
|
+
|
|
115
|
+
if isinstance(spec, dict):
|
|
116
|
+
func = spec["func"]
|
|
117
|
+
output = spec.get("output", col)
|
|
118
|
+
return [(func, output)]
|
|
119
|
+
|
|
120
|
+
if isinstance(spec, list):
|
|
121
|
+
result = []
|
|
122
|
+
for entry in spec:
|
|
123
|
+
if isinstance(entry, str):
|
|
124
|
+
result.append((entry, col))
|
|
125
|
+
elif isinstance(entry, dict):
|
|
126
|
+
func = entry["func"]
|
|
127
|
+
output = entry.get("output", col)
|
|
128
|
+
result.append((func, output))
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"Column '{col}': spec must be a string, dict, or list, "
|
|
133
|
+
f"got {type(spec).__name__}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _apply_func(grouped, col: str, func: str) -> pd.Series:
|
|
138
|
+
"""Apply a single aggregation function to a grouped column."""
|
|
139
|
+
if func == "mode":
|
|
140
|
+
return grouped[col].agg(lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else None)
|
|
141
|
+
if func == "nunique":
|
|
142
|
+
return grouped[col].nunique()
|
|
143
|
+
return grouped[col].agg(func)
|
|
@@ -34,6 +34,40 @@ class TestCompileSpec:
|
|
|
34
34
|
name, transformer = pipeline.steps[0]
|
|
35
35
|
assert not isinstance(transformer, DataFrameColumnTransformer)
|
|
36
36
|
|
|
37
|
+
def test_step_level_columns_on_drop_actually_drops(self):
|
|
38
|
+
# Specs commonly declare columns at the step level with empty params.
|
|
39
|
+
# DropColumnsTransformer is column-aware: wrapping it in
|
|
40
|
+
# DataFrameColumnTransformer turns the drop into a no-op (the wrapper
|
|
41
|
+
# re-attaches the "transformed" — i.e. untouched — columns).
|
|
42
|
+
spec = PipelineSpec(steps=[
|
|
43
|
+
StepSpec(id="drop", type="DropColumnsTransformer",
|
|
44
|
+
columns=["id", "count"], params={}),
|
|
45
|
+
])
|
|
46
|
+
pipeline = compile_spec(spec)
|
|
47
|
+
df = pd.DataFrame({"id": [1, 2], "count": [3, 4], "value": [5.0, 6.0]})
|
|
48
|
+
result = pipeline.fit_transform(df)
|
|
49
|
+
assert list(result.columns) == ["value"]
|
|
50
|
+
|
|
51
|
+
def test_step_level_columns_injected_into_column_aware_transformer(self):
|
|
52
|
+
spec = PipelineSpec(steps=[
|
|
53
|
+
StepSpec(id="drop", type="DropColumnsTransformer",
|
|
54
|
+
columns=["id"], params={}),
|
|
55
|
+
])
|
|
56
|
+
pipeline = compile_spec(spec)
|
|
57
|
+
_, transformer = pipeline.steps[0]
|
|
58
|
+
assert not isinstance(transformer, DataFrameColumnTransformer)
|
|
59
|
+
assert transformer.columns == ["id"]
|
|
60
|
+
|
|
61
|
+
def test_params_columns_take_precedence_over_step_columns(self):
|
|
62
|
+
spec = PipelineSpec(steps=[
|
|
63
|
+
StepSpec(id="drop", type="DropColumnsTransformer",
|
|
64
|
+
columns=["id"], params={"columns": ["count"]}),
|
|
65
|
+
])
|
|
66
|
+
pipeline = compile_spec(spec)
|
|
67
|
+
_, transformer = pipeline.steps[0]
|
|
68
|
+
assert not isinstance(transformer, DataFrameColumnTransformer)
|
|
69
|
+
assert transformer.columns == ["count"]
|
|
70
|
+
|
|
37
71
|
def test_unknown_type_raises(self):
|
|
38
72
|
spec = PipelineSpec(steps=[
|
|
39
73
|
StepSpec(id="bad", type="FakeTransformer"),
|
|
@@ -118,3 +152,51 @@ class TestCompileSpec:
|
|
|
118
152
|
assert "id" not in result.columns
|
|
119
153
|
assert "value" in result.columns
|
|
120
154
|
assert not result["value"].isna().any()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestUnsupportedParamFiltering:
|
|
158
|
+
"""Specs may be generated in an env with a newer sklearn than the one
|
|
159
|
+
compiling them (2026-07: autotrain on sklearn 1.7 emitted
|
|
160
|
+
KBinsDiscretizer(quantile_method=...), which the API's sklearn 1.6
|
|
161
|
+
constructor rejects). Unknown constructor params must be dropped with a
|
|
162
|
+
warning instead of crashing compilation."""
|
|
163
|
+
|
|
164
|
+
def test_unknown_param_is_dropped_with_warning(self, caplog):
|
|
165
|
+
import logging
|
|
166
|
+
|
|
167
|
+
spec = PipelineSpec(steps=[
|
|
168
|
+
StepSpec(
|
|
169
|
+
id="bin",
|
|
170
|
+
type="KBinsDiscretizer",
|
|
171
|
+
columns=["age"],
|
|
172
|
+
params={
|
|
173
|
+
"n_bins": 4,
|
|
174
|
+
"encode": "ordinal",
|
|
175
|
+
"strategy": "quantile",
|
|
176
|
+
"definitely_not_a_real_sklearn_param": "x",
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
])
|
|
180
|
+
with caplog.at_level(logging.WARNING):
|
|
181
|
+
pipeline = compile_spec(spec)
|
|
182
|
+
|
|
183
|
+
_, transformer = pipeline.steps[0]
|
|
184
|
+
inner = transformer.transformer
|
|
185
|
+
assert inner.n_bins == 4
|
|
186
|
+
assert not hasattr(inner, "definitely_not_a_real_sklearn_param")
|
|
187
|
+
assert any("definitely_not_a_real_sklearn_param" in r.message
|
|
188
|
+
for r in caplog.records)
|
|
189
|
+
|
|
190
|
+
def test_supported_params_pass_through_unchanged(self):
|
|
191
|
+
spec = PipelineSpec(steps=[
|
|
192
|
+
StepSpec(
|
|
193
|
+
id="bin",
|
|
194
|
+
type="KBinsDiscretizer",
|
|
195
|
+
columns=["age"],
|
|
196
|
+
params={"n_bins": 3, "encode": "ordinal"},
|
|
197
|
+
),
|
|
198
|
+
])
|
|
199
|
+
pipeline = compile_spec(spec)
|
|
200
|
+
_, transformer = pipeline.steps[0]
|
|
201
|
+
assert transformer.transformer.n_bins == 3
|
|
202
|
+
assert transformer.transformer.encode == "ordinal"
|
|
@@ -9,6 +9,7 @@ from xplainable_preprocessing.transformers.datetime_extract import DateTimeExtra
|
|
|
9
9
|
from xplainable_preprocessing.transformers.drop_columns import DropColumnsTransformer
|
|
10
10
|
from xplainable_preprocessing.transformers.fill_missing import FillMissingTransformer
|
|
11
11
|
from xplainable_preprocessing.transformers.grouped_lag import GroupedLagTransformer
|
|
12
|
+
from xplainable_preprocessing.transformers.groupby_agg import GroupByAggTransformer
|
|
12
13
|
from xplainable_preprocessing.transformers.rename_columns import RenameColumnsTransformer
|
|
13
14
|
from xplainable_preprocessing.transformers.rolling_agg import RollingAggTransformer
|
|
14
15
|
from xplainable_preprocessing.transformers.text_clean import TextCleanTransformer
|
|
@@ -226,3 +227,128 @@ class TestRollingAggTransformer:
|
|
|
226
227
|
t = RollingAggTransformer(columns=["v"], operation="invalid")
|
|
227
228
|
with pytest.raises(ValueError, match="Unknown operation"):
|
|
228
229
|
t.fit(pd.DataFrame({"v": [1]}))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TestGroupByAggTransformer:
|
|
233
|
+
@pytest.fixture
|
|
234
|
+
def orders_df(self):
|
|
235
|
+
return pd.DataFrame({
|
|
236
|
+
"customer_id": [1, 1, 1, 2, 2, 3],
|
|
237
|
+
"country": ["US", "US", "US", "UK", "UK", "CA"],
|
|
238
|
+
"revenue": [100.0, 200.0, 150.0, 300.0, 250.0, 50.0],
|
|
239
|
+
"quantity": [2, 3, 1, 4, 2, 1],
|
|
240
|
+
"product_id": [10, 20, 10, 30, 30, 40],
|
|
241
|
+
"category": ["A", "B", "A", "C", "C", "A"],
|
|
242
|
+
"returned": [0, 1, 0, 0, 0, 1],
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
def test_basic_string_agg(self, orders_df):
|
|
246
|
+
t = GroupByAggTransformer(
|
|
247
|
+
group_by=["customer_id"],
|
|
248
|
+
aggregations={"country": "first"},
|
|
249
|
+
)
|
|
250
|
+
result = t.fit_transform(orders_df)
|
|
251
|
+
assert len(result) == 3
|
|
252
|
+
assert "customer_id" in result.columns
|
|
253
|
+
assert "country" in result.columns
|
|
254
|
+
assert result.loc[result["customer_id"] == 1, "country"].iloc[0] == "US"
|
|
255
|
+
|
|
256
|
+
def test_dict_with_rename(self, orders_df):
|
|
257
|
+
t = GroupByAggTransformer(
|
|
258
|
+
group_by=["customer_id"],
|
|
259
|
+
aggregations={
|
|
260
|
+
"revenue": {"func": "sum", "output": "total_spent"},
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
result = t.fit_transform(orders_df)
|
|
264
|
+
assert "total_spent" in result.columns
|
|
265
|
+
assert "revenue" not in result.columns
|
|
266
|
+
assert result.loc[result["customer_id"] == 1, "total_spent"].iloc[0] == 450.0
|
|
267
|
+
|
|
268
|
+
def test_list_multiple_aggs(self, orders_df):
|
|
269
|
+
t = GroupByAggTransformer(
|
|
270
|
+
group_by=["customer_id"],
|
|
271
|
+
aggregations={
|
|
272
|
+
"revenue": [
|
|
273
|
+
{"func": "sum", "output": "total_revenue"},
|
|
274
|
+
{"func": "mean", "output": "avg_revenue"},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
result = t.fit_transform(orders_df)
|
|
279
|
+
assert "total_revenue" in result.columns
|
|
280
|
+
assert "avg_revenue" in result.columns
|
|
281
|
+
assert result.loc[result["customer_id"] == 1, "total_revenue"].iloc[0] == 450.0
|
|
282
|
+
assert result.loc[result["customer_id"] == 1, "avg_revenue"].iloc[0] == 150.0
|
|
283
|
+
|
|
284
|
+
def test_nunique(self, orders_df):
|
|
285
|
+
t = GroupByAggTransformer(
|
|
286
|
+
group_by=["customer_id"],
|
|
287
|
+
aggregations={
|
|
288
|
+
"product_id": {"func": "nunique", "output": "unique_products"},
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
result = t.fit_transform(orders_df)
|
|
292
|
+
assert result.loc[result["customer_id"] == 1, "unique_products"].iloc[0] == 2
|
|
293
|
+
assert result.loc[result["customer_id"] == 2, "unique_products"].iloc[0] == 1
|
|
294
|
+
|
|
295
|
+
def test_mode(self, orders_df):
|
|
296
|
+
t = GroupByAggTransformer(
|
|
297
|
+
group_by=["customer_id"],
|
|
298
|
+
aggregations={
|
|
299
|
+
"category": {"func": "mode", "output": "primary_category"},
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
result = t.fit_transform(orders_df)
|
|
303
|
+
assert result.loc[result["customer_id"] == 1, "primary_category"].iloc[0] == "A"
|
|
304
|
+
assert result.loc[result["customer_id"] == 2, "primary_category"].iloc[0] == "C"
|
|
305
|
+
|
|
306
|
+
def test_count(self, orders_df):
|
|
307
|
+
t = GroupByAggTransformer(
|
|
308
|
+
group_by=["customer_id"],
|
|
309
|
+
aggregations={
|
|
310
|
+
"revenue": {"func": "count", "output": "orders_count"},
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
result = t.fit_transform(orders_df)
|
|
314
|
+
assert result.loc[result["customer_id"] == 1, "orders_count"].iloc[0] == 3
|
|
315
|
+
assert result.loc[result["customer_id"] == 3, "orders_count"].iloc[0] == 1
|
|
316
|
+
|
|
317
|
+
def test_invalid_function(self, orders_df):
|
|
318
|
+
t = GroupByAggTransformer(
|
|
319
|
+
group_by=["customer_id"],
|
|
320
|
+
aggregations={"revenue": "bogus"},
|
|
321
|
+
)
|
|
322
|
+
with pytest.raises(ValueError, match="unknown function"):
|
|
323
|
+
t.fit(orders_df)
|
|
324
|
+
|
|
325
|
+
def test_empty_group_by_raises(self, orders_df):
|
|
326
|
+
t = GroupByAggTransformer(
|
|
327
|
+
group_by=[],
|
|
328
|
+
aggregations={"revenue": "sum"},
|
|
329
|
+
)
|
|
330
|
+
t.fit(orders_df)
|
|
331
|
+
with pytest.raises(ValueError, match="group_by must be specified"):
|
|
332
|
+
t.transform(orders_df)
|
|
333
|
+
|
|
334
|
+
def test_combined_spec(self, orders_df):
|
|
335
|
+
"""Full integration: multiple agg types in one spec."""
|
|
336
|
+
t = GroupByAggTransformer(
|
|
337
|
+
group_by=["customer_id"],
|
|
338
|
+
aggregations={
|
|
339
|
+
"country": "first",
|
|
340
|
+
"revenue": [
|
|
341
|
+
{"func": "sum", "output": "total_spent"},
|
|
342
|
+
{"func": "mean", "output": "avg_order_value"},
|
|
343
|
+
],
|
|
344
|
+
"product_id": {"func": "nunique", "output": "unique_products"},
|
|
345
|
+
"category": {"func": "mode", "output": "top_category"},
|
|
346
|
+
"returned": {"func": "sum", "output": "return_count"},
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
result = t.fit_transform(orders_df)
|
|
350
|
+
assert len(result) == 3
|
|
351
|
+
assert set(result.columns) == {
|
|
352
|
+
"customer_id", "country", "total_spent", "avg_order_value",
|
|
353
|
+
"unique_products", "top_category", "return_count",
|
|
354
|
+
}
|
{xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/.github/workflows/publish-pypi.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/docs/dag-pipeline-proposal.md
RENAMED
|
File without changes
|
|
File without changes
|
{xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/docs/feature-store-proposal.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xplainable_preprocessing-0.2.1 → xplainable_preprocessing-0.2.3}/tests/test_serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|