xplainable-preprocessing 0.1.0__tar.gz → 0.2.1__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/.github/workflows/publish-pypi.yml +42 -0
- xplainable_preprocessing-0.2.1/.gitignore +1 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/PKG-INFO +1 -1
- xplainable_preprocessing-0.2.1/README.md +164 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/pyproject.toml +1 -1
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/registry.py +6 -0
- xplainable_preprocessing-0.2.1/src/xplainable_preprocessing/schema.py +372 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/__init__.py +18 -12
- xplainable_preprocessing-0.2.1/src/xplainable_preprocessing/transformers/clip.py +45 -0
- xplainable_preprocessing-0.2.1/src/xplainable_preprocessing/transformers/missing_flag.py +48 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/schema.py +0 -71
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/docs/dag-pipeline-proposal.md +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/docs/feature-pipeline-architectures.md +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/docs/feature-store-proposal.md +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/__init__.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/compiler.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/pipeline.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/preview.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/sandbox.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/serialization.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/category_condense.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/datetime_extract.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/drop_columns.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/expression.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/fill_missing.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/grouped_lag.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/rename_columns.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/rolling_agg.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/text_clean.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/src/xplainable_preprocessing/transformers/type_cast.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/__init__.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/test_compiler.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/test_preview.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/test_sandbox.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/test_schema.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/test_serialization.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/test_transformers/__init__.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/test_transformers/test_all_transformers.py +0 -0
- {xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/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
|
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Pydantic models for pipeline specification with mutation support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, field_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StepSpec(BaseModel):
|
|
11
|
+
"""A single preprocessing step in the pipeline.
|
|
12
|
+
|
|
13
|
+
The ``outputs``, ``modifies``, and ``drops`` fields form the step's
|
|
14
|
+
**column contract**. They are populated after the first fit via
|
|
15
|
+
``PipelineSpec.enrich_from_deltas()`` and enable cascade analysis
|
|
16
|
+
for step mutations (remove, reorder) without recompiling the pipeline.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
type: str
|
|
21
|
+
columns: Optional[List[str]] = None
|
|
22
|
+
params: Dict = {}
|
|
23
|
+
description: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
# Column contract — populated after first fit via enrich_from_deltas()
|
|
26
|
+
outputs: Optional[List[str]] = None # columns this step CREATES
|
|
27
|
+
modifies: Optional[List[str]] = None # columns this step CHANGES in place
|
|
28
|
+
drops: Optional[List[str]] = None # columns this step REMOVES
|
|
29
|
+
|
|
30
|
+
@field_validator("id")
|
|
31
|
+
@classmethod
|
|
32
|
+
def id_must_be_non_empty(cls, v: str) -> str:
|
|
33
|
+
if not v.strip():
|
|
34
|
+
raise ValueError("Step id must be non-empty")
|
|
35
|
+
return v
|
|
36
|
+
|
|
37
|
+
@field_validator("type")
|
|
38
|
+
@classmethod
|
|
39
|
+
def type_must_be_non_empty(cls, v: str) -> str:
|
|
40
|
+
if not v.strip():
|
|
41
|
+
raise ValueError("Step type must be non-empty")
|
|
42
|
+
return v
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RemovalAnalysis(BaseModel):
|
|
46
|
+
"""Result of analyzing what happens when a step is removed."""
|
|
47
|
+
|
|
48
|
+
step_id: str
|
|
49
|
+
cascade_remove: List[str] = [] # step IDs that must also be removed
|
|
50
|
+
columns_lost: List[str] = [] # columns that would disappear
|
|
51
|
+
columns_restored: List[str] = [] # columns that were dropped and would reappear
|
|
52
|
+
safe: bool = True # True if no downstream steps are affected
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PipelineSpec(BaseModel):
|
|
56
|
+
"""Full pipeline specification containing ordered steps.
|
|
57
|
+
|
|
58
|
+
Supports mutation methods (``remove_step``, ``insert_step``,
|
|
59
|
+
``reorder_step``) that return new ``PipelineSpec`` instances.
|
|
60
|
+
Cascade analysis via ``analyze_removal`` works from the column
|
|
61
|
+
contract without needing to recompile the pipeline.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
version: str = "2.0"
|
|
65
|
+
steps: List[StepSpec] = []
|
|
66
|
+
|
|
67
|
+
@field_validator("steps")
|
|
68
|
+
@classmethod
|
|
69
|
+
def step_ids_must_be_unique(cls, v: List[StepSpec]) -> List[StepSpec]:
|
|
70
|
+
ids = [step.id for step in v]
|
|
71
|
+
if len(ids) != len(set(ids)):
|
|
72
|
+
duplicates = [id_ for id_ in ids if ids.count(id_) > 1]
|
|
73
|
+
raise ValueError(f"Duplicate step ids: {set(duplicates)}")
|
|
74
|
+
return v
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# Helpers
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def get_step(self, step_id: str) -> Optional[StepSpec]:
|
|
81
|
+
"""Return a step by ID, or None."""
|
|
82
|
+
for step in self.steps:
|
|
83
|
+
if step.id == step_id:
|
|
84
|
+
return step
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def step_index(self, step_id: str) -> int:
|
|
88
|
+
"""Return the index of a step by ID. Raises ValueError if not found."""
|
|
89
|
+
for i, step in enumerate(self.steps):
|
|
90
|
+
if step.id == step_id:
|
|
91
|
+
return i
|
|
92
|
+
raise ValueError(f"Step '{step_id}' not found in spec")
|
|
93
|
+
|
|
94
|
+
def steps_after(self, step_id: str) -> List[StepSpec]:
|
|
95
|
+
"""Return all steps that come after the given step."""
|
|
96
|
+
idx = self.step_index(step_id)
|
|
97
|
+
return self.steps[idx + 1:]
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def is_enriched(self) -> bool:
|
|
101
|
+
"""True if column contracts have been populated on all steps."""
|
|
102
|
+
return all(
|
|
103
|
+
step.outputs is not None
|
|
104
|
+
for step in self.steps
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Enrichment — write column contracts from deltas
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def enrich_from_deltas(self, step_deltas: list[dict]) -> "PipelineSpec":
|
|
112
|
+
"""Return a new spec with column contracts populated from step deltas.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
step_deltas : list[dict]
|
|
117
|
+
Output of ``preview.compute_step_deltas()``. Each entry has
|
|
118
|
+
``step_id`` and ``delta: {added, updated, dropped}``.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
PipelineSpec
|
|
123
|
+
A new spec instance with ``outputs``/``modifies``/``drops``
|
|
124
|
+
populated on each step.
|
|
125
|
+
"""
|
|
126
|
+
delta_map = {d["step_id"]: d for d in step_deltas}
|
|
127
|
+
enriched_steps = []
|
|
128
|
+
for step in self.steps:
|
|
129
|
+
entry = delta_map.get(step.id, {})
|
|
130
|
+
delta = entry.get("delta", {})
|
|
131
|
+
updates = {
|
|
132
|
+
"outputs": delta.get("added", []),
|
|
133
|
+
"modifies": delta.get("updated", []),
|
|
134
|
+
"drops": delta.get("dropped", []),
|
|
135
|
+
}
|
|
136
|
+
# columns (inputs) is left as-is — only the LLM-declared value
|
|
137
|
+
# is used for dependency resolution. Outputs/modifies/drops are
|
|
138
|
+
# sufficient for cascade analysis.
|
|
139
|
+
enriched_steps.append(step.model_copy(update=updates))
|
|
140
|
+
return PipelineSpec(version=self.version, steps=enriched_steps)
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# Cascade analysis
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def analyze_removal(self, step_id: str) -> RemovalAnalysis:
|
|
147
|
+
"""Analyze what happens if a step is removed.
|
|
148
|
+
|
|
149
|
+
Uses the column contract (``outputs``/``drops``) to determine
|
|
150
|
+
which downstream steps would lose their input columns and must
|
|
151
|
+
also be removed (cascade).
|
|
152
|
+
|
|
153
|
+
Works without recompiling the pipeline — purely from the spec
|
|
154
|
+
metadata. Requires the spec to be enriched first.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
step_id : str
|
|
159
|
+
The ID of the step to analyze removing.
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
RemovalAnalysis
|
|
164
|
+
"""
|
|
165
|
+
step = self.get_step(step_id)
|
|
166
|
+
if step is None:
|
|
167
|
+
raise ValueError(f"Step '{step_id}' not found")
|
|
168
|
+
|
|
169
|
+
# Columns that would disappear if this step is removed
|
|
170
|
+
lost_columns = set(step.outputs or [])
|
|
171
|
+
|
|
172
|
+
# Walk downstream and find cascade
|
|
173
|
+
cascade = []
|
|
174
|
+
for other in self.steps_after(step_id):
|
|
175
|
+
inputs = set(other.columns or [])
|
|
176
|
+
if inputs & lost_columns:
|
|
177
|
+
cascade.append(other.id)
|
|
178
|
+
# This step's outputs are also lost in the cascade
|
|
179
|
+
lost_columns.update(other.outputs or [])
|
|
180
|
+
|
|
181
|
+
# Columns that this step drops would be restored
|
|
182
|
+
restored = list(step.drops or [])
|
|
183
|
+
|
|
184
|
+
return RemovalAnalysis(
|
|
185
|
+
step_id=step_id,
|
|
186
|
+
cascade_remove=cascade,
|
|
187
|
+
columns_lost=sorted(lost_columns),
|
|
188
|
+
columns_restored=sorted(restored),
|
|
189
|
+
safe=len(cascade) == 0,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------------
|
|
193
|
+
# Mutations — all return NEW PipelineSpec instances (immutable pattern)
|
|
194
|
+
# ------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def remove_step(self, step_id: str, cascade: bool = True) -> "PipelineSpec":
|
|
197
|
+
"""Return a new spec with the step (and optionally its cascade) removed.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
step_id : str
|
|
202
|
+
The step to remove.
|
|
203
|
+
cascade : bool
|
|
204
|
+
If True, also remove downstream steps that depend on columns
|
|
205
|
+
created by this step. If False, only remove this single step
|
|
206
|
+
(caller is responsible for ensuring validity).
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
PipelineSpec
|
|
211
|
+
"""
|
|
212
|
+
analysis = self.analyze_removal(step_id)
|
|
213
|
+
remove_ids = {step_id}
|
|
214
|
+
if cascade:
|
|
215
|
+
remove_ids.update(analysis.cascade_remove)
|
|
216
|
+
return PipelineSpec(
|
|
217
|
+
version=self.version,
|
|
218
|
+
steps=[s for s in self.steps if s.id not in remove_ids],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def insert_step(
|
|
222
|
+
self,
|
|
223
|
+
step: StepSpec,
|
|
224
|
+
after: Optional[str] = None,
|
|
225
|
+
) -> "PipelineSpec":
|
|
226
|
+
"""Return a new spec with a step inserted.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
step : StepSpec
|
|
231
|
+
The step to insert.
|
|
232
|
+
after : str or None
|
|
233
|
+
Insert after this step ID. If None, append to the end.
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
PipelineSpec
|
|
238
|
+
"""
|
|
239
|
+
new_steps = list(self.steps)
|
|
240
|
+
if after is None:
|
|
241
|
+
new_steps.append(step)
|
|
242
|
+
else:
|
|
243
|
+
idx = self.step_index(after)
|
|
244
|
+
new_steps.insert(idx + 1, step)
|
|
245
|
+
return PipelineSpec(version=self.version, steps=new_steps)
|
|
246
|
+
|
|
247
|
+
def reorder_step(self, step_id: str, new_index: int) -> "PipelineSpec":
|
|
248
|
+
"""Return a new spec with a step moved to a new position.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
step_id : str
|
|
253
|
+
The step to move.
|
|
254
|
+
new_index : int
|
|
255
|
+
The target index (0-based).
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
PipelineSpec
|
|
260
|
+
"""
|
|
261
|
+
idx = self.step_index(step_id)
|
|
262
|
+
step = self.steps[idx]
|
|
263
|
+
remaining = [s for s in self.steps if s.id != step_id]
|
|
264
|
+
clamped = max(0, min(new_index, len(remaining)))
|
|
265
|
+
remaining.insert(clamped, step)
|
|
266
|
+
return PipelineSpec(version=self.version, steps=remaining)
|
|
267
|
+
|
|
268
|
+
def update_step_params(self, step_id: str, params: Dict) -> "PipelineSpec":
|
|
269
|
+
"""Return a new spec with a step's params updated.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
step_id : str
|
|
274
|
+
The step to update.
|
|
275
|
+
params : dict
|
|
276
|
+
New params to merge into the step's existing params.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
PipelineSpec
|
|
281
|
+
"""
|
|
282
|
+
new_steps = []
|
|
283
|
+
for step in self.steps:
|
|
284
|
+
if step.id == step_id:
|
|
285
|
+
merged = {**step.params, **params}
|
|
286
|
+
new_steps.append(step.model_copy(update={"params": merged}))
|
|
287
|
+
else:
|
|
288
|
+
new_steps.append(step)
|
|
289
|
+
return PipelineSpec(version=self.version, steps=new_steps)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def optimize(self) -> "PipelineSpec":
|
|
293
|
+
"""Return a new spec with steps in optimal execution order.
|
|
294
|
+
|
|
295
|
+
Topologically sorts steps so that any step producing a column
|
|
296
|
+
comes before steps that consume it. Independent steps retain
|
|
297
|
+
their relative order.
|
|
298
|
+
|
|
299
|
+
Requires column contracts (``outputs``) and declared ``columns``
|
|
300
|
+
(inputs) on each step. If the spec is not enriched, returns
|
|
301
|
+
a copy with the original order.
|
|
302
|
+
"""
|
|
303
|
+
if not self.is_enriched:
|
|
304
|
+
return self.model_copy()
|
|
305
|
+
|
|
306
|
+
from collections import defaultdict
|
|
307
|
+
|
|
308
|
+
step_map = {s.id: s for s in self.steps}
|
|
309
|
+
ids = [s.id for s in self.steps]
|
|
310
|
+
|
|
311
|
+
# Build producer map: column → step that creates it
|
|
312
|
+
producers: dict[str, str] = {}
|
|
313
|
+
for s in self.steps:
|
|
314
|
+
for col in s.outputs or []:
|
|
315
|
+
if col not in producers:
|
|
316
|
+
producers[col] = s.id
|
|
317
|
+
|
|
318
|
+
# Build adjacency from declared inputs
|
|
319
|
+
adj: dict[str, set[str]] = defaultdict(set)
|
|
320
|
+
in_deg: dict[str, int] = {sid: 0 for sid in ids}
|
|
321
|
+
for s in self.steps:
|
|
322
|
+
for col in s.columns or []:
|
|
323
|
+
prod = producers.get(col)
|
|
324
|
+
if prod and prod != s.id and prod in step_map:
|
|
325
|
+
if prod not in adj or s.id not in adj[prod]:
|
|
326
|
+
adj[prod].add(s.id)
|
|
327
|
+
in_deg[s.id] += 1
|
|
328
|
+
|
|
329
|
+
# Kahn's algorithm — stable (preserves original order within tiers)
|
|
330
|
+
ordered: list[str] = []
|
|
331
|
+
remaining = set(ids)
|
|
332
|
+
while remaining:
|
|
333
|
+
ready = [sid for sid in ids if sid in remaining and in_deg.get(sid, 0) == 0]
|
|
334
|
+
if not ready:
|
|
335
|
+
ordered.extend(sorted(remaining))
|
|
336
|
+
break
|
|
337
|
+
ordered.extend(ready)
|
|
338
|
+
for sid in ready:
|
|
339
|
+
remaining.discard(sid)
|
|
340
|
+
for nb in adj.get(sid, set()):
|
|
341
|
+
in_deg[nb] = max(in_deg.get(nb, 1) - 1, 0)
|
|
342
|
+
|
|
343
|
+
return PipelineSpec(
|
|
344
|
+
version=self.version,
|
|
345
|
+
steps=[step_map[sid] for sid in ordered if sid in step_map],
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def validate_spec(spec: PipelineSpec) -> None:
|
|
350
|
+
"""Validate a PipelineSpec. Raises ValueError if invalid.
|
|
351
|
+
|
|
352
|
+
Checks beyond Pydantic validation:
|
|
353
|
+
- All non-custom types must exist in the registry
|
|
354
|
+
- Custom steps must have 'code' and 'class_name' in params
|
|
355
|
+
"""
|
|
356
|
+
from xplainable_preprocessing.registry import REGISTRY
|
|
357
|
+
|
|
358
|
+
for step in spec.steps:
|
|
359
|
+
if step.type == "custom":
|
|
360
|
+
if "code" not in step.params:
|
|
361
|
+
raise ValueError(
|
|
362
|
+
f"Step '{step.id}': custom type requires 'code' in params"
|
|
363
|
+
)
|
|
364
|
+
if "class_name" not in step.params:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"Step '{step.id}': custom type requires 'class_name' in params"
|
|
367
|
+
)
|
|
368
|
+
elif step.type not in REGISTRY:
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"Step '{step.id}': unknown type '{step.type}'. "
|
|
371
|
+
f"Available: {sorted(REGISTRY.keys())}"
|
|
372
|
+
)
|
|
@@ -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,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
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Pydantic models for pipeline specification."""
|
|
2
|
-
|
|
3
|
-
from typing import Dict, List, Optional
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, field_validator
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class StepSpec(BaseModel):
|
|
9
|
-
"""A single preprocessing step in the pipeline."""
|
|
10
|
-
|
|
11
|
-
id: str
|
|
12
|
-
type: str
|
|
13
|
-
columns: Optional[List[str]] = None
|
|
14
|
-
params: Dict = {}
|
|
15
|
-
description: Optional[str] = None
|
|
16
|
-
|
|
17
|
-
@field_validator("id")
|
|
18
|
-
@classmethod
|
|
19
|
-
def id_must_be_non_empty(cls, v: str) -> str:
|
|
20
|
-
if not v.strip():
|
|
21
|
-
raise ValueError("Step id must be non-empty")
|
|
22
|
-
return v
|
|
23
|
-
|
|
24
|
-
@field_validator("type")
|
|
25
|
-
@classmethod
|
|
26
|
-
def type_must_be_non_empty(cls, v: str) -> str:
|
|
27
|
-
if not v.strip():
|
|
28
|
-
raise ValueError("Step type must be non-empty")
|
|
29
|
-
return v
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class PipelineSpec(BaseModel):
|
|
33
|
-
"""Full pipeline specification containing ordered steps."""
|
|
34
|
-
|
|
35
|
-
version: str = "2.0"
|
|
36
|
-
steps: List[StepSpec] = []
|
|
37
|
-
|
|
38
|
-
@field_validator("steps")
|
|
39
|
-
@classmethod
|
|
40
|
-
def step_ids_must_be_unique(cls, v: List[StepSpec]) -> List[StepSpec]:
|
|
41
|
-
ids = [step.id for step in v]
|
|
42
|
-
if len(ids) != len(set(ids)):
|
|
43
|
-
duplicates = [id_ for id_ in ids if ids.count(id_) > 1]
|
|
44
|
-
raise ValueError(f"Duplicate step ids: {set(duplicates)}")
|
|
45
|
-
return v
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def validate_spec(spec: PipelineSpec) -> None:
|
|
49
|
-
"""Validate a PipelineSpec. Raises ValueError if invalid.
|
|
50
|
-
|
|
51
|
-
Checks beyond Pydantic validation:
|
|
52
|
-
- All non-custom types must exist in the registry
|
|
53
|
-
- Custom steps must have 'code' and 'class_name' in params
|
|
54
|
-
"""
|
|
55
|
-
from xplainable_preprocessing.registry import REGISTRY
|
|
56
|
-
|
|
57
|
-
for step in spec.steps:
|
|
58
|
-
if step.type == "custom":
|
|
59
|
-
if "code" not in step.params:
|
|
60
|
-
raise ValueError(
|
|
61
|
-
f"Step '{step.id}': custom type requires 'code' in params"
|
|
62
|
-
)
|
|
63
|
-
if "class_name" not in step.params:
|
|
64
|
-
raise ValueError(
|
|
65
|
-
f"Step '{step.id}': custom type requires 'class_name' in params"
|
|
66
|
-
)
|
|
67
|
-
elif step.type not in REGISTRY:
|
|
68
|
-
raise ValueError(
|
|
69
|
-
f"Step '{step.id}': unknown type '{step.type}'. "
|
|
70
|
-
f"Available: {sorted(REGISTRY.keys())}"
|
|
71
|
-
)
|
{xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/docs/dag-pipeline-proposal.md
RENAMED
|
File without changes
|
|
File without changes
|
{xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/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
|
{xplainable_preprocessing-0.1.0 → xplainable_preprocessing-0.2.1}/tests/test_serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|