xplainable-preprocessing 0.2.0__tar.gz → 0.2.2__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.2/.github/workflows/publish-pypi.yml +42 -0
- xplainable_preprocessing-0.2.2/.gitignore +1 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/PKG-INFO +1 -1
- xplainable_preprocessing-0.2.2/README.md +164 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/pyproject.toml +1 -1
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/registry.py +6 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/__init__.py +18 -12
- xplainable_preprocessing-0.2.2/src/xplainable_preprocessing/transformers/clip.py +45 -0
- xplainable_preprocessing-0.2.2/src/xplainable_preprocessing/transformers/groupby_agg.py +143 -0
- xplainable_preprocessing-0.2.2/src/xplainable_preprocessing/transformers/missing_flag.py +48 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_transformers/test_all_transformers.py +126 -0
- xplainable_preprocessing-0.2.0/scripts/release.sh +0 -292
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/docs/dag-pipeline-proposal.md +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/docs/feature-pipeline-architectures.md +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/docs/feature-store-proposal.md +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/__init__.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/compiler.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/pipeline.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/preview.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/sandbox.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/schema.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/serialization.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/category_condense.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/datetime_extract.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/drop_columns.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/expression.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/fill_missing.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/grouped_lag.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/rename_columns.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/rolling_agg.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/text_clean.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/src/xplainable_preprocessing/transformers/type_cast.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/__init__.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_compiler.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_preview.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_sandbox.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_schema.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_serialization.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_transformers/__init__.py +0 -0
- {xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_transformers/test_expression.py +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.11'
|
|
22
|
+
|
|
23
|
+
- name: Install build tools
|
|
24
|
+
run: pip install build
|
|
25
|
+
|
|
26
|
+
- name: Build package
|
|
27
|
+
run: python -m build
|
|
28
|
+
|
|
29
|
+
- name: Publish to PyPI
|
|
30
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
31
|
+
with:
|
|
32
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
33
|
+
|
|
34
|
+
- name: Create GitHub Release
|
|
35
|
+
env:
|
|
36
|
+
GH_TOKEN: ${{ github.token }}
|
|
37
|
+
run: |
|
|
38
|
+
VERSION=${GITHUB_REF_NAME}
|
|
39
|
+
gh release create "$VERSION" dist/* \
|
|
40
|
+
--title "$VERSION" \
|
|
41
|
+
--generate-notes \
|
|
42
|
+
--latest
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.env
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# xplainable-preprocessing
|
|
2
|
+
|
|
3
|
+
Shared preprocessing pipeline library for [xplainable](https://xplainable.io). Define, compile, fit, serialise, and apply data transformation pipelines using a declarative JSON spec.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install xplainable-preprocessing
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Define a pipeline as JSON
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from xplainable_preprocessing.schema import PipelineSpec
|
|
17
|
+
from xplainable_preprocessing.compiler import compile_spec
|
|
18
|
+
|
|
19
|
+
spec = PipelineSpec(
|
|
20
|
+
version="2.0",
|
|
21
|
+
steps=[
|
|
22
|
+
{"id": "drop_ids", "type": "DropColumnsTransformer", "params": {"columns": ["customer_id", "row_id"]}},
|
|
23
|
+
{"id": "fill_numeric", "type": "FillMissingTransformer", "columns": ["tenure", "charges"], "params": {"strategies": {"tenure": "median", "charges": "median"}}},
|
|
24
|
+
{"id": "clip_outliers", "type": "ClipTransformer", "columns": ["charges"], "params": {"min_val": 0, "max_val": 500}},
|
|
25
|
+
{"id": "missing_flags", "type": "MissingFlagTransformer", "columns": ["tenure", "charges"]},
|
|
26
|
+
{"id": "condense_plan", "type": "CategoryCondenseTransformer", "columns": ["plan_type"], "params": {"max_categories": 10}},
|
|
27
|
+
{"id": "extract_dates", "type": "DateTimeExtractTransformer", "columns": ["signup_date"], "params": {"components": ["month", "dayofweek"], "drop_original": true}},
|
|
28
|
+
]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
pipeline = compile_spec(spec)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Fit and transform
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import pandas as pd
|
|
38
|
+
|
|
39
|
+
df = pd.read_csv("data.csv")
|
|
40
|
+
pipeline.fit(df)
|
|
41
|
+
transformed = pipeline.transform(df)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Serialise and load
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from xplainable_preprocessing.serialization import save_pipeline, load_pipeline
|
|
48
|
+
|
|
49
|
+
# Save
|
|
50
|
+
binary = save_pipeline(pipeline)
|
|
51
|
+
|
|
52
|
+
# Load
|
|
53
|
+
loaded = load_pipeline(binary)
|
|
54
|
+
result = loaded.transform(new_data)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Available Transformers
|
|
58
|
+
|
|
59
|
+
### Data Cleaning
|
|
60
|
+
| Transformer | Description |
|
|
61
|
+
|-------------|-------------|
|
|
62
|
+
| `DropColumnsTransformer` | Drop specified columns |
|
|
63
|
+
| `RenameColumnsTransformer` | Rename columns |
|
|
64
|
+
| `TypeCastTransformer` | Cast column dtypes (e.g. str to float64, with error coercion) |
|
|
65
|
+
| `ClipTransformer` | Clip numeric values outside (min, max) to NaN |
|
|
66
|
+
|
|
67
|
+
### Missing Values
|
|
68
|
+
| Transformer | Description |
|
|
69
|
+
|-------------|-------------|
|
|
70
|
+
| `FillMissingTransformer` | Fill nulls with median, mode, constant, or other strategies |
|
|
71
|
+
| `MissingFlagTransformer` | Create binary 0/1 indicator columns for nulls |
|
|
72
|
+
|
|
73
|
+
### Feature Engineering
|
|
74
|
+
| Transformer | Description |
|
|
75
|
+
|-------------|-------------|
|
|
76
|
+
| `ExpressionTransformer` | Create derived columns via pandas expressions |
|
|
77
|
+
| `DateTimeExtractTransformer` | Extract year, month, dayofweek, etc. from datetime columns |
|
|
78
|
+
| `GroupByAggTransformer` | Aggregate features by group (count, sum, mean) |
|
|
79
|
+
| `GroupedLagTransformer` | Create lagged features within groups |
|
|
80
|
+
| `RollingAggTransformer` | Rolling window aggregations |
|
|
81
|
+
|
|
82
|
+
### Categorical
|
|
83
|
+
| Transformer | Description |
|
|
84
|
+
|-------------|-------------|
|
|
85
|
+
| `CategoryCondenseTransformer` | Condense high-cardinality categoricals to top N + "Other" |
|
|
86
|
+
| `TextCleanTransformer` | Clean text: lowercase, strip, remove HTML/extra whitespace |
|
|
87
|
+
|
|
88
|
+
### Scaling (use with caution -- breaks xplainable model explainability)
|
|
89
|
+
| Transformer | Description |
|
|
90
|
+
|-------------|-------------|
|
|
91
|
+
| `StandardScaler` | Standardise to zero mean, unit variance |
|
|
92
|
+
| `MinMaxScaler` | Scale to [0, 1] range |
|
|
93
|
+
| `RobustScaler` | Scale using median and IQR |
|
|
94
|
+
| `PowerTransformer` | Apply power transform for normality |
|
|
95
|
+
| `QuantileTransformer` | Transform to uniform or normal distribution |
|
|
96
|
+
|
|
97
|
+
> **Note:** Scaling transformers are available but should NOT be used with xplainable models. xplainable models are inherently explainable and require raw feature values. Scaling destroys interpretability.
|
|
98
|
+
|
|
99
|
+
### Other sklearn
|
|
100
|
+
| Transformer | Description |
|
|
101
|
+
|-------------|-------------|
|
|
102
|
+
| `SimpleImputer` | sklearn's imputer |
|
|
103
|
+
| `OneHotEncoder` | One-hot encode categoricals |
|
|
104
|
+
| `OrdinalEncoder` | Ordinal encode categoricals |
|
|
105
|
+
| `KBinsDiscretizer` | Discretise continuous features into bins |
|
|
106
|
+
| `Binarizer` | Threshold features to binary |
|
|
107
|
+
|
|
108
|
+
## Pipeline Spec Format
|
|
109
|
+
|
|
110
|
+
Pipelines are defined as JSON for portability across the xplainable platform, MCP tools, and API:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"version": "2.0",
|
|
115
|
+
"steps": [
|
|
116
|
+
{
|
|
117
|
+
"id": "step_name",
|
|
118
|
+
"type": "TransformerName",
|
|
119
|
+
"columns": ["col1", "col2"],
|
|
120
|
+
"params": {"param1": "value1"},
|
|
121
|
+
"description": "Optional description"
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- `id`: unique step identifier
|
|
128
|
+
- `type`: transformer class name (from the registry)
|
|
129
|
+
- `columns`: optional list of columns to apply to (wraps in `DataFrameColumnTransformer`)
|
|
130
|
+
- `params`: constructor arguments for the transformer
|
|
131
|
+
- `description`: optional human-readable description
|
|
132
|
+
|
|
133
|
+
## Architecture
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
PipelineSpec (JSON)
|
|
137
|
+
↓ compile_spec()
|
|
138
|
+
DataFramePipeline (sklearn Pipeline of transformers)
|
|
139
|
+
↓ fit() / transform()
|
|
140
|
+
Transformed DataFrame
|
|
141
|
+
↓ save_pipeline()
|
|
142
|
+
Binary (cloudpickle)
|
|
143
|
+
↓ load_pipeline()
|
|
144
|
+
DataFramePipeline (restored)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Key modules:
|
|
148
|
+
- `schema.py` -- Pydantic models for `PipelineSpec` and `StepSpec`
|
|
149
|
+
- `compiler.py` -- compiles JSON spec into sklearn pipeline
|
|
150
|
+
- `pipeline.py` -- `DataFramePipeline` wrapper around sklearn `Pipeline`
|
|
151
|
+
- `registry.py` -- maps transformer names to classes, generates LLM-ready catalog
|
|
152
|
+
- `serialization.py` -- cloudpickle save/load
|
|
153
|
+
- `preview.py` -- before/after preview with schema deltas
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
pip install -e ".[dev]"
|
|
159
|
+
pytest
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
|
@@ -20,11 +20,14 @@ from sklearn.preprocessing import (
|
|
|
20
20
|
|
|
21
21
|
from xplainable_preprocessing.transformers import (
|
|
22
22
|
CategoryCondenseTransformer,
|
|
23
|
+
ClipTransformer,
|
|
23
24
|
DateTimeExtractTransformer,
|
|
24
25
|
DropColumnsTransformer,
|
|
25
26
|
ExpressionTransformer,
|
|
26
27
|
FillMissingTransformer,
|
|
28
|
+
GroupByAggTransformer,
|
|
27
29
|
GroupedLagTransformer,
|
|
30
|
+
MissingFlagTransformer,
|
|
28
31
|
RenameColumnsTransformer,
|
|
29
32
|
RollingAggTransformer,
|
|
30
33
|
TextCleanTransformer,
|
|
@@ -55,6 +58,9 @@ REGISTRY: dict[str, type] = {
|
|
|
55
58
|
"DateTimeExtractTransformer": DateTimeExtractTransformer,
|
|
56
59
|
"GroupedLagTransformer": GroupedLagTransformer,
|
|
57
60
|
"RollingAggTransformer": RollingAggTransformer,
|
|
61
|
+
"GroupByAggTransformer": GroupByAggTransformer,
|
|
62
|
+
"ClipTransformer": ClipTransformer,
|
|
63
|
+
"MissingFlagTransformer": MissingFlagTransformer,
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
|
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
from xplainable_preprocessing.transformers.expression import ExpressionTransformer
|
|
2
|
-
from xplainable_preprocessing.transformers.drop_columns import DropColumnsTransformer
|
|
3
|
-
from xplainable_preprocessing.transformers.rename_columns import RenameColumnsTransformer
|
|
4
|
-
from xplainable_preprocessing.transformers.type_cast import TypeCastTransformer
|
|
5
|
-
from xplainable_preprocessing.transformers.fill_missing import FillMissingTransformer
|
|
6
1
|
from xplainable_preprocessing.transformers.category_condense import CategoryCondenseTransformer
|
|
7
|
-
from xplainable_preprocessing.transformers.
|
|
2
|
+
from xplainable_preprocessing.transformers.clip import ClipTransformer
|
|
8
3
|
from xplainable_preprocessing.transformers.datetime_extract import DateTimeExtractTransformer
|
|
4
|
+
from xplainable_preprocessing.transformers.drop_columns import DropColumnsTransformer
|
|
5
|
+
from xplainable_preprocessing.transformers.expression import ExpressionTransformer
|
|
6
|
+
from xplainable_preprocessing.transformers.fill_missing import FillMissingTransformer
|
|
7
|
+
from xplainable_preprocessing.transformers.groupby_agg import GroupByAggTransformer
|
|
9
8
|
from xplainable_preprocessing.transformers.grouped_lag import GroupedLagTransformer
|
|
9
|
+
from xplainable_preprocessing.transformers.missing_flag import MissingFlagTransformer
|
|
10
|
+
from xplainable_preprocessing.transformers.rename_columns import RenameColumnsTransformer
|
|
10
11
|
from xplainable_preprocessing.transformers.rolling_agg import RollingAggTransformer
|
|
12
|
+
from xplainable_preprocessing.transformers.text_clean import TextCleanTransformer
|
|
13
|
+
from xplainable_preprocessing.transformers.type_cast import TypeCastTransformer
|
|
11
14
|
|
|
12
15
|
__all__ = [
|
|
13
|
-
"ExpressionTransformer",
|
|
14
|
-
"DropColumnsTransformer",
|
|
15
|
-
"RenameColumnsTransformer",
|
|
16
|
-
"TypeCastTransformer",
|
|
17
|
-
"FillMissingTransformer",
|
|
18
16
|
"CategoryCondenseTransformer",
|
|
19
|
-
"
|
|
17
|
+
"ClipTransformer",
|
|
20
18
|
"DateTimeExtractTransformer",
|
|
19
|
+
"DropColumnsTransformer",
|
|
20
|
+
"ExpressionTransformer",
|
|
21
|
+
"FillMissingTransformer",
|
|
22
|
+
"GroupByAggTransformer",
|
|
21
23
|
"GroupedLagTransformer",
|
|
24
|
+
"MissingFlagTransformer",
|
|
25
|
+
"RenameColumnsTransformer",
|
|
22
26
|
"RollingAggTransformer",
|
|
27
|
+
"TextCleanTransformer",
|
|
28
|
+
"TypeCastTransformer",
|
|
23
29
|
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Clip transformer -- clips numeric values outside a range to NaN."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClipTransformer(BaseEstimator, TransformerMixin):
|
|
11
|
+
"""Clip numeric values outside (min_val, max_val) to NaN.
|
|
12
|
+
|
|
13
|
+
Useful for nulling out physically impossible values
|
|
14
|
+
(e.g. BMI of 0, 2090 push-ups) so they don't create
|
|
15
|
+
junk bins in the model.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
min_val : float | None
|
|
20
|
+
Values strictly below this are set to NaN. None means no lower bound.
|
|
21
|
+
max_val : float | None
|
|
22
|
+
Values strictly above this are set to NaN. None means no upper bound.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
min_val: float | None = None,
|
|
28
|
+
max_val: float | None = None,
|
|
29
|
+
):
|
|
30
|
+
self.min_val = min_val
|
|
31
|
+
self.max_val = max_val
|
|
32
|
+
|
|
33
|
+
def fit(self, X: pd.DataFrame, y=None):
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
|
|
37
|
+
X = X.copy()
|
|
38
|
+
for col in X.columns:
|
|
39
|
+
if not pd.api.types.is_numeric_dtype(X[col]):
|
|
40
|
+
continue
|
|
41
|
+
if self.min_val is not None:
|
|
42
|
+
X.loc[X[col] < self.min_val, col] = np.nan
|
|
43
|
+
if self.max_val is not None:
|
|
44
|
+
X.loc[X[col] > self.max_val, col] = np.nan
|
|
45
|
+
return X
|
|
@@ -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)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Missing flag transformer -- creates binary indicators for null values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MissingFlagTransformer(BaseEstimator, TransformerMixin):
|
|
10
|
+
"""Create binary 0/1 indicator columns for null values.
|
|
11
|
+
|
|
12
|
+
For each specified column, adds a new column ``{col}_missing``
|
|
13
|
+
that is 1 where the original value is null and 0 otherwise.
|
|
14
|
+
Useful when missingness itself is informative (e.g. "physical
|
|
15
|
+
testing not completed" predicts injury risk).
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
suffix : str
|
|
20
|
+
Suffix appended to column names for the flag columns.
|
|
21
|
+
Default ``"_missing"``.
|
|
22
|
+
drop_original : bool
|
|
23
|
+
If True, drop the original columns after creating flags.
|
|
24
|
+
Default False.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
suffix: str = "_missing",
|
|
30
|
+
drop_original: bool = False,
|
|
31
|
+
):
|
|
32
|
+
self.suffix = suffix
|
|
33
|
+
self.drop_original = drop_original
|
|
34
|
+
|
|
35
|
+
def fit(self, X: pd.DataFrame, y=None):
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
|
|
39
|
+
X = X.copy()
|
|
40
|
+
flag_cols = {}
|
|
41
|
+
for col in X.columns:
|
|
42
|
+
flag_cols[f"{col}{self.suffix}"] = X[col].isna().astype(int)
|
|
43
|
+
flags = pd.DataFrame(flag_cols, index=X.index)
|
|
44
|
+
if self.drop_original:
|
|
45
|
+
X = pd.concat([flags], axis=1)
|
|
46
|
+
else:
|
|
47
|
+
X = pd.concat([X, flags], axis=1)
|
|
48
|
+
return X
|
|
@@ -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
|
+
}
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Simple release script for xplainable-preprocessing
|
|
3
|
-
|
|
4
|
-
set -e
|
|
5
|
-
|
|
6
|
-
# Colors for output
|
|
7
|
-
RED='\033[0;31m'
|
|
8
|
-
GREEN='\033[0;32m'
|
|
9
|
-
YELLOW='\033[1;33m'
|
|
10
|
-
BLUE='\033[0;34m'
|
|
11
|
-
NC='\033[0m' # No Color
|
|
12
|
-
|
|
13
|
-
# Functions
|
|
14
|
-
print_header() {
|
|
15
|
-
echo -e "${BLUE}$1${NC}"
|
|
16
|
-
echo "=================================="
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
print_success() {
|
|
20
|
-
echo -e "${GREEN}✅ $1${NC}"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
print_warning() {
|
|
24
|
-
echo -e "${YELLOW}⚠️ $1${NC}"
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
print_error() {
|
|
28
|
-
echo -e "${RED}❌ $1${NC}"
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
# Parse arguments
|
|
32
|
-
VERSION=""
|
|
33
|
-
INCREMENT=""
|
|
34
|
-
DRY_RUN=false
|
|
35
|
-
PUBLISH_PYPI=false
|
|
36
|
-
|
|
37
|
-
while [[ $# -gt 0 ]]; do
|
|
38
|
-
case $1 in
|
|
39
|
-
-v|--version)
|
|
40
|
-
VERSION="$2"
|
|
41
|
-
shift 2
|
|
42
|
-
;;
|
|
43
|
-
-i|--increment)
|
|
44
|
-
INCREMENT="$2"
|
|
45
|
-
shift 2
|
|
46
|
-
;;
|
|
47
|
-
--dry-run)
|
|
48
|
-
DRY_RUN=true
|
|
49
|
-
shift
|
|
50
|
-
;;
|
|
51
|
-
--publish)
|
|
52
|
-
PUBLISH_PYPI=true
|
|
53
|
-
shift
|
|
54
|
-
;;
|
|
55
|
-
-h|--help)
|
|
56
|
-
echo "Usage: $0 [OPTIONS]"
|
|
57
|
-
echo ""
|
|
58
|
-
echo "Options:"
|
|
59
|
-
echo " -v, --version VERSION Specific version (e.g., 1.2.3)"
|
|
60
|
-
echo " -i, --increment TYPE Auto-increment: major, minor, or patch"
|
|
61
|
-
echo " --dry-run Show what would be done"
|
|
62
|
-
echo " --publish Publish to PyPI after building"
|
|
63
|
-
echo " -h, --help Show this help"
|
|
64
|
-
echo ""
|
|
65
|
-
echo "Examples:"
|
|
66
|
-
echo " $0 --version 0.2.0"
|
|
67
|
-
echo " $0 --increment patch"
|
|
68
|
-
echo " $0 --increment minor --publish"
|
|
69
|
-
echo " $0 --increment minor --dry-run"
|
|
70
|
-
exit 0
|
|
71
|
-
;;
|
|
72
|
-
*)
|
|
73
|
-
print_error "Unknown option: $1"
|
|
74
|
-
exit 1
|
|
75
|
-
;;
|
|
76
|
-
esac
|
|
77
|
-
done
|
|
78
|
-
|
|
79
|
-
# Validate arguments
|
|
80
|
-
if [[ -z "$VERSION" && -z "$INCREMENT" ]]; then
|
|
81
|
-
print_error "Must specify either --version or --increment"
|
|
82
|
-
echo "Run $0 --help for usage"
|
|
83
|
-
exit 1
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
if [[ -n "$VERSION" && -n "$INCREMENT" ]]; then
|
|
87
|
-
print_error "Cannot specify both --version and --increment"
|
|
88
|
-
exit 1
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
print_header "🚀 xplainable-preprocessing Release"
|
|
92
|
-
|
|
93
|
-
# Check if we're in the right directory
|
|
94
|
-
if [[ ! -f "pyproject.toml" ]]; then
|
|
95
|
-
print_error "Not in project root (no pyproject.toml found)"
|
|
96
|
-
exit 1
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
# Check git status
|
|
100
|
-
print_header "📋 Pre-release Checks"
|
|
101
|
-
|
|
102
|
-
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
|
103
|
-
print_error "Not in a git repository"
|
|
104
|
-
exit 1
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
if [[ -n $(git status --porcelain) ]]; then
|
|
108
|
-
print_error "Uncommitted changes detected"
|
|
109
|
-
git status --short
|
|
110
|
-
exit 1
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
|
-
CURRENT_BRANCH=$(git branch --show-current)
|
|
114
|
-
if [[ "$CURRENT_BRANCH" != "main" && "$CURRENT_BRANCH" != "master" ]]; then
|
|
115
|
-
print_warning "Currently on branch '$CURRENT_BRANCH'"
|
|
116
|
-
read -p "Continue anyway? (y/N): " -n 1 -r
|
|
117
|
-
echo
|
|
118
|
-
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
119
|
-
exit 1
|
|
120
|
-
fi
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
print_success "Git repository is clean"
|
|
124
|
-
|
|
125
|
-
# Run tests before release
|
|
126
|
-
print_header "🧪 Running Tests"
|
|
127
|
-
if python -m pytest tests/ -q; then
|
|
128
|
-
print_success "All tests passed"
|
|
129
|
-
else
|
|
130
|
-
print_error "Tests failed — aborting release"
|
|
131
|
-
exit 1
|
|
132
|
-
fi
|
|
133
|
-
|
|
134
|
-
# Get current version from pyproject.toml
|
|
135
|
-
CURRENT_VERSION=$(grep '^version =' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
|
|
136
|
-
|
|
137
|
-
if [[ -z "$CURRENT_VERSION" ]]; then
|
|
138
|
-
print_error "Could not determine current version"
|
|
139
|
-
exit 1
|
|
140
|
-
fi
|
|
141
|
-
|
|
142
|
-
print_success "Current version: $CURRENT_VERSION"
|
|
143
|
-
|
|
144
|
-
# Determine new version
|
|
145
|
-
if [[ -n "$VERSION" ]]; then
|
|
146
|
-
NEW_VERSION="${VERSION#v}" # Remove 'v' prefix if present
|
|
147
|
-
else
|
|
148
|
-
# Auto-increment
|
|
149
|
-
IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION"
|
|
150
|
-
MAJOR=${VERSION_PARTS[0]}
|
|
151
|
-
MINOR=${VERSION_PARTS[1]}
|
|
152
|
-
PATCH=${VERSION_PARTS[2]}
|
|
153
|
-
|
|
154
|
-
case $INCREMENT in
|
|
155
|
-
major)
|
|
156
|
-
MAJOR=$((MAJOR + 1))
|
|
157
|
-
MINOR=0
|
|
158
|
-
PATCH=0
|
|
159
|
-
;;
|
|
160
|
-
minor)
|
|
161
|
-
MINOR=$((MINOR + 1))
|
|
162
|
-
PATCH=0
|
|
163
|
-
;;
|
|
164
|
-
patch)
|
|
165
|
-
PATCH=$((PATCH + 1))
|
|
166
|
-
;;
|
|
167
|
-
*)
|
|
168
|
-
print_error "Invalid increment type: $INCREMENT"
|
|
169
|
-
exit 1
|
|
170
|
-
;;
|
|
171
|
-
esac
|
|
172
|
-
|
|
173
|
-
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
TAG="v$NEW_VERSION"
|
|
177
|
-
|
|
178
|
-
print_header "📦 Release Plan"
|
|
179
|
-
echo "Current: $CURRENT_VERSION"
|
|
180
|
-
echo "New: $NEW_VERSION"
|
|
181
|
-
echo "Tag: $TAG"
|
|
182
|
-
|
|
183
|
-
if [[ "$DRY_RUN" == true ]]; then
|
|
184
|
-
print_header "🔍 DRY RUN - Would perform:"
|
|
185
|
-
echo "1. Clean build artifacts"
|
|
186
|
-
echo "2. Update version in pyproject.toml"
|
|
187
|
-
echo "3. Commit version changes"
|
|
188
|
-
echo "4. Create git tag $TAG"
|
|
189
|
-
echo "5. Build package (wheel and sdist)"
|
|
190
|
-
echo "6. Push changes and tag"
|
|
191
|
-
if [[ "$PUBLISH_PYPI" == true ]]; then
|
|
192
|
-
echo "7. Publish package to PyPI"
|
|
193
|
-
echo "8. Create GitHub release (if gh CLI available)"
|
|
194
|
-
else
|
|
195
|
-
echo "7. Create GitHub release (if gh CLI available)"
|
|
196
|
-
fi
|
|
197
|
-
exit 0
|
|
198
|
-
fi
|
|
199
|
-
|
|
200
|
-
# Confirm
|
|
201
|
-
read -p "Create release v$NEW_VERSION? (y/N): " -n 1 -r
|
|
202
|
-
echo
|
|
203
|
-
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
204
|
-
print_error "Release cancelled"
|
|
205
|
-
exit 1
|
|
206
|
-
fi
|
|
207
|
-
|
|
208
|
-
print_header "🔨 Creating Release"
|
|
209
|
-
|
|
210
|
-
# Clean up build artifacts
|
|
211
|
-
print_success "Cleaning up build artifacts..."
|
|
212
|
-
rm -rf build/ dist/ *.egg-info/ src/*.egg-info/ || true
|
|
213
|
-
|
|
214
|
-
# Update version in pyproject.toml
|
|
215
|
-
print_success "Updating version..."
|
|
216
|
-
sed -i.bak "s/^version = .*/version = \"$NEW_VERSION\"/" pyproject.toml && rm pyproject.toml.bak
|
|
217
|
-
print_success "Updated pyproject.toml"
|
|
218
|
-
|
|
219
|
-
# Commit changes
|
|
220
|
-
print_success "Committing version changes..."
|
|
221
|
-
git add .
|
|
222
|
-
git commit -m "chore: bump version to $NEW_VERSION"
|
|
223
|
-
|
|
224
|
-
# Create tag
|
|
225
|
-
print_success "Creating tag $TAG..."
|
|
226
|
-
RELEASE_DATE=$(date +%Y-%m-%d)
|
|
227
|
-
git tag -a "$TAG" -m "Release $TAG — $RELEASE_DATE"
|
|
228
|
-
|
|
229
|
-
# Build package
|
|
230
|
-
print_success "Building package..."
|
|
231
|
-
python -m build
|
|
232
|
-
|
|
233
|
-
# Push changes
|
|
234
|
-
print_success "Pushing changes..."
|
|
235
|
-
git push
|
|
236
|
-
git push --tags
|
|
237
|
-
|
|
238
|
-
# Publish to PyPI if requested
|
|
239
|
-
if [[ "$PUBLISH_PYPI" == true ]]; then
|
|
240
|
-
print_success "Publishing to PyPI..."
|
|
241
|
-
|
|
242
|
-
if ! command -v twine &> /dev/null; then
|
|
243
|
-
print_error "twine not found"
|
|
244
|
-
echo "Install with: pip install twine"
|
|
245
|
-
exit 1
|
|
246
|
-
fi
|
|
247
|
-
|
|
248
|
-
if [[ -z "$TWINE_PASSWORD" && ! -f ~/.pypirc ]]; then
|
|
249
|
-
print_error "PyPI credentials not configured"
|
|
250
|
-
echo "Set up credentials with one of:"
|
|
251
|
-
echo " 1. Environment: export TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-..."
|
|
252
|
-
echo " 2. Config file: ~/.pypirc"
|
|
253
|
-
exit 1
|
|
254
|
-
fi
|
|
255
|
-
|
|
256
|
-
if twine upload dist/*; then
|
|
257
|
-
print_success "Package published to PyPI!"
|
|
258
|
-
echo "🔗 View at: https://pypi.org/project/xplainable-preprocessing/$NEW_VERSION/"
|
|
259
|
-
else
|
|
260
|
-
print_error "PyPI upload failed"
|
|
261
|
-
exit 1
|
|
262
|
-
fi
|
|
263
|
-
fi
|
|
264
|
-
|
|
265
|
-
# Create GitHub release if gh CLI is available
|
|
266
|
-
if command -v gh &> /dev/null; then
|
|
267
|
-
if gh auth status &> /dev/null; then
|
|
268
|
-
print_success "Creating GitHub release..."
|
|
269
|
-
|
|
270
|
-
gh release create "$TAG" \
|
|
271
|
-
--title "v$NEW_VERSION" \
|
|
272
|
-
--notes "## xplainable-preprocessing v$NEW_VERSION — $RELEASE_DATE
|
|
273
|
-
|
|
274
|
-
### Installation
|
|
275
|
-
|
|
276
|
-
\`\`\`bash
|
|
277
|
-
pip install xplainable-preprocessing==$NEW_VERSION
|
|
278
|
-
\`\`\`" \
|
|
279
|
-
--latest
|
|
280
|
-
|
|
281
|
-
print_success "GitHub release created!"
|
|
282
|
-
else
|
|
283
|
-
print_warning "GitHub CLI not authenticated, skipping release creation"
|
|
284
|
-
fi
|
|
285
|
-
else
|
|
286
|
-
print_warning "GitHub CLI not found, skipping release creation"
|
|
287
|
-
fi
|
|
288
|
-
|
|
289
|
-
print_header "🎉 RELEASE COMPLETE!"
|
|
290
|
-
echo "Version: $NEW_VERSION"
|
|
291
|
-
echo "Tag: $TAG"
|
|
292
|
-
echo "PyPI: https://pypi.org/project/xplainable-preprocessing/$NEW_VERSION/"
|
{xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/docs/dag-pipeline-proposal.md
RENAMED
|
File without changes
|
|
File without changes
|
{xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/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
|
{xplainable_preprocessing-0.2.0 → xplainable_preprocessing-0.2.2}/tests/test_serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|