xplainable-preprocessing 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xplainable_preprocessing-0.1.0/PKG-INFO +13 -0
- xplainable_preprocessing-0.1.0/docs/dag-pipeline-proposal.md +305 -0
- xplainable_preprocessing-0.1.0/docs/feature-pipeline-architectures.md +363 -0
- xplainable_preprocessing-0.1.0/docs/feature-store-proposal.md +530 -0
- xplainable_preprocessing-0.1.0/pyproject.toml +28 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/__init__.py +20 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/compiler.py +68 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/pipeline.py +87 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/preview.py +137 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/registry.py +108 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/sandbox.py +113 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/schema.py +71 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/serialization.py +20 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/__init__.py +23 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/category_condense.py +50 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/datetime_extract.py +72 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/drop_columns.py +25 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/expression.py +35 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/fill_missing.py +71 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/grouped_lag.py +55 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/rename_columns.py +25 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/rolling_agg.py +79 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/text_clean.py +70 -0
- xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/type_cast.py +42 -0
- xplainable_preprocessing-0.1.0/tests/__init__.py +0 -0
- xplainable_preprocessing-0.1.0/tests/test_compiler.py +120 -0
- xplainable_preprocessing-0.1.0/tests/test_preview.py +90 -0
- xplainable_preprocessing-0.1.0/tests/test_sandbox.py +132 -0
- xplainable_preprocessing-0.1.0/tests/test_schema.py +109 -0
- xplainable_preprocessing-0.1.0/tests/test_serialization.py +83 -0
- xplainable_preprocessing-0.1.0/tests/test_transformers/__init__.py +0 -0
- xplainable_preprocessing-0.1.0/tests/test_transformers/test_all_transformers.py +228 -0
- xplainable_preprocessing-0.1.0/tests/test_transformers/test_expression.py +30 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xplainable-preprocessing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared preprocessing pipeline package for xplainable
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: cloudpickle>=3.0
|
|
7
|
+
Requires-Dist: numpy>=1.24
|
|
8
|
+
Requires-Dist: pandas>=2.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Requires-Dist: scikit-learn>=1.3
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# DAG-Based Feature Pipeline
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
The current `PipelineSpec` is a flat ordered list of steps. This creates three issues:
|
|
6
|
+
|
|
7
|
+
1. **Ordering burden on the LLM** — the LLM must emit steps in correct dependency order. If step C needs the output of steps A and B, the LLM must place A and B before C. With complex feature graphs this is error-prone.
|
|
8
|
+
|
|
9
|
+
2. **No parallelism** — independent branches (e.g. extract datetime features vs compute rolling aggregates) run sequentially even though they don't share state.
|
|
10
|
+
|
|
11
|
+
3. **Post-hoc dependency analysis** — the autotrain agent already has `analyze_feature_dependencies_tool` which makes an extra LLM call to figure out execution order. A DAG makes this structural.
|
|
12
|
+
|
|
13
|
+
## Proposal
|
|
14
|
+
|
|
15
|
+
Add an optional `depends_on` field to `StepSpec`. When present on any step, the compiler builds a DAG and resolves execution order via topological sort. When absent on all steps, the pipeline behaves identically to today (flat sequential).
|
|
16
|
+
|
|
17
|
+
### Spec Changes
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# schema.py
|
|
21
|
+
class StepSpec(BaseModel):
|
|
22
|
+
id: str
|
|
23
|
+
type: str
|
|
24
|
+
columns: Optional[List[str]] = None
|
|
25
|
+
params: Dict = {}
|
|
26
|
+
description: Optional[str] = None
|
|
27
|
+
depends_on: Optional[List[str]] = None # NEW — step IDs this step requires
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# schema.py
|
|
32
|
+
class PipelineSpec(BaseModel):
|
|
33
|
+
version: str = "2.0" # stays 2.0 — depends_on is additive, not breaking
|
|
34
|
+
steps: List[StepSpec] = []
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Example Spec
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"version": "2.0",
|
|
42
|
+
"steps": [
|
|
43
|
+
{
|
|
44
|
+
"id": "extract_dow",
|
|
45
|
+
"type": "DateTimeExtractTransformer",
|
|
46
|
+
"columns": ["order_date"],
|
|
47
|
+
"params": {"components": ["dayofweek"]},
|
|
48
|
+
"depends_on": []
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "rolling_sales",
|
|
52
|
+
"type": "RollingAggTransformer",
|
|
53
|
+
"columns": ["sales"],
|
|
54
|
+
"params": {"group_by": ["store_id"], "window": 7, "operation": "mean", "order_by": "order_date"},
|
|
55
|
+
"depends_on": []
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "dow_sales_interaction",
|
|
59
|
+
"type": "ExpressionTransformer",
|
|
60
|
+
"params": {"expression": "order_date_dayofweek * sales_mean_7", "output_column": "dow_sales_x"},
|
|
61
|
+
"depends_on": ["extract_dow", "rolling_sales"]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "drop_raw_date",
|
|
65
|
+
"type": "DropColumnsTransformer",
|
|
66
|
+
"params": {"columns": ["order_date"]},
|
|
67
|
+
"depends_on": ["extract_dow", "rolling_sales"]
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This forms the graph:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
extract_dow ──┬──> dow_sales_interaction
|
|
77
|
+
├──> drop_raw_date
|
|
78
|
+
rolling_sales ┘
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Steps `extract_dow` and `rolling_sales` have no dependencies so they can run in parallel. `dow_sales_interaction` and `drop_raw_date` wait for both to complete, then they can also run in parallel.
|
|
82
|
+
|
|
83
|
+
## Implementation Plan
|
|
84
|
+
|
|
85
|
+
### Phase 1: Schema + Validation (no runtime changes)
|
|
86
|
+
|
|
87
|
+
**Files**: `schema.py`
|
|
88
|
+
|
|
89
|
+
1. Add `depends_on: Optional[List[str]] = None` to `StepSpec`
|
|
90
|
+
2. Add validation in `PipelineSpec`:
|
|
91
|
+
- All `depends_on` IDs must reference existing step IDs
|
|
92
|
+
- No cycles (topological sort must succeed)
|
|
93
|
+
- No self-references
|
|
94
|
+
3. Add `validate_spec()` check: if any step has `depends_on`, verify the DAG is valid
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# In validate_spec():
|
|
98
|
+
def _validate_dag(spec: PipelineSpec) -> None:
|
|
99
|
+
"""Validate DAG constraints if any step uses depends_on."""
|
|
100
|
+
has_dag = any(step.depends_on is not None for step in spec.steps)
|
|
101
|
+
if not has_dag:
|
|
102
|
+
return # flat pipeline, no DAG validation needed
|
|
103
|
+
|
|
104
|
+
step_ids = {step.id for step in spec.steps}
|
|
105
|
+
for step in spec.steps:
|
|
106
|
+
if step.depends_on:
|
|
107
|
+
for dep in step.depends_on:
|
|
108
|
+
if dep not in step_ids:
|
|
109
|
+
raise ValueError(f"Step '{step.id}' depends on unknown step '{dep}'")
|
|
110
|
+
if dep == step.id:
|
|
111
|
+
raise ValueError(f"Step '{step.id}' cannot depend on itself")
|
|
112
|
+
|
|
113
|
+
# Cycle detection via topological sort
|
|
114
|
+
_topological_sort(spec.steps) # raises ValueError on cycle
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Tests**: Spec with valid DAG parses. Spec with cycle raises. Spec with missing dep raises. Flat spec (no `depends_on`) still works.
|
|
118
|
+
|
|
119
|
+
### Phase 2: DAG Compiler
|
|
120
|
+
|
|
121
|
+
**Files**: `compiler.py`, new `dag.py`
|
|
122
|
+
|
|
123
|
+
Create a `compile_dag()` function that:
|
|
124
|
+
1. Builds an adjacency graph from `depends_on`
|
|
125
|
+
2. Topologically sorts steps into execution "stages" (groups of independent steps)
|
|
126
|
+
3. Returns a `DAGPipeline` (or falls back to `DataFramePipeline` for flat specs)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# dag.py
|
|
130
|
+
from collections import defaultdict, deque
|
|
131
|
+
|
|
132
|
+
def topological_stages(steps: list[StepSpec]) -> list[list[StepSpec]]:
|
|
133
|
+
"""Group steps into parallel execution stages via Kahn's algorithm.
|
|
134
|
+
|
|
135
|
+
Returns a list of stages. Steps within a stage have no mutual
|
|
136
|
+
dependencies and can run in parallel.
|
|
137
|
+
"""
|
|
138
|
+
step_map = {s.id: s for s in steps}
|
|
139
|
+
in_degree = {s.id: 0 for s in steps}
|
|
140
|
+
dependents = defaultdict(list) # step_id -> list of steps that depend on it
|
|
141
|
+
|
|
142
|
+
for step in steps:
|
|
143
|
+
for dep in (step.depends_on or []):
|
|
144
|
+
in_degree[step.id] += 1
|
|
145
|
+
dependents[dep].append(step.id)
|
|
146
|
+
|
|
147
|
+
stages = []
|
|
148
|
+
queue = deque(sid for sid, deg in in_degree.items() if deg == 0)
|
|
149
|
+
|
|
150
|
+
while queue:
|
|
151
|
+
# All items currently in queue have in_degree 0 — they form a parallel stage
|
|
152
|
+
stage = []
|
|
153
|
+
next_queue = deque()
|
|
154
|
+
while queue:
|
|
155
|
+
sid = queue.popleft()
|
|
156
|
+
stage.append(step_map[sid])
|
|
157
|
+
for dependent in dependents[sid]:
|
|
158
|
+
in_degree[dependent] -= 1
|
|
159
|
+
if in_degree[dependent] == 0:
|
|
160
|
+
next_queue.append(dependent)
|
|
161
|
+
stages.append(stage)
|
|
162
|
+
queue = next_queue
|
|
163
|
+
|
|
164
|
+
# Check for cycles
|
|
165
|
+
processed = sum(len(s) for s in stages)
|
|
166
|
+
if processed != len(steps):
|
|
167
|
+
raise ValueError("Cycle detected in step dependencies")
|
|
168
|
+
|
|
169
|
+
return stages
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Update `compile_spec()`:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
# compiler.py
|
|
176
|
+
def compile_spec(spec: PipelineSpec) -> DataFramePipeline:
|
|
177
|
+
has_dag = any(step.depends_on is not None for step in spec.steps)
|
|
178
|
+
if has_dag:
|
|
179
|
+
return _compile_dag(spec)
|
|
180
|
+
return _compile_flat(spec) # existing logic
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Phase 3: DAGPipeline Runtime
|
|
184
|
+
|
|
185
|
+
**Files**: new `dag_pipeline.py`
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
class DAGPipeline(BaseEstimator, TransformerMixin):
|
|
189
|
+
"""Pipeline that executes steps in DAG-resolved stage order.
|
|
190
|
+
|
|
191
|
+
Steps within the same stage are independent and can be run
|
|
192
|
+
concurrently (future optimization). Steps across stages run
|
|
193
|
+
sequentially — each stage sees the DataFrame produced by all
|
|
194
|
+
prior stages.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, stages: list[list[tuple[str, TransformerMixin]]]):
|
|
198
|
+
# stages[i] = [(step_id, transformer), ...] — all independent
|
|
199
|
+
self.stages = stages
|
|
200
|
+
|
|
201
|
+
def fit(self, X: pd.DataFrame, y=None):
|
|
202
|
+
Xt = X.copy()
|
|
203
|
+
for stage in self.stages:
|
|
204
|
+
# All transformers in a stage fit on the same Xt
|
|
205
|
+
for name, transformer in stage:
|
|
206
|
+
transformer.fit(Xt, y)
|
|
207
|
+
# Then apply all transforms (order within stage doesn't matter
|
|
208
|
+
# because they read from the same Xt snapshot)
|
|
209
|
+
Xt_snapshot = Xt.copy()
|
|
210
|
+
for name, transformer in stage:
|
|
211
|
+
result = transformer.transform(Xt_snapshot)
|
|
212
|
+
# Merge result columns back into Xt
|
|
213
|
+
new_cols = [c for c in result.columns if c not in Xt.columns]
|
|
214
|
+
for col in new_cols:
|
|
215
|
+
Xt[col] = result[col]
|
|
216
|
+
# Handle column modifications (existing cols that changed)
|
|
217
|
+
modified_cols = [c for c in result.columns if c in Xt.columns and c in (transformer.columns if hasattr(transformer, 'columns') else [])]
|
|
218
|
+
for col in modified_cols:
|
|
219
|
+
Xt[col] = result[col]
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
|
|
223
|
+
# Same logic but without fit
|
|
224
|
+
...
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Key design decisions**:
|
|
228
|
+
|
|
229
|
+
- **Snapshot per stage**: Each stage gets a snapshot of the DataFrame. Transforms within a stage read from the snapshot (not from each other's outputs). This prevents order-dependence within a stage.
|
|
230
|
+
- **Merge strategy**: New columns are added. Modified columns are overwritten. Dropped columns are removed. This needs careful handling — see "Tricky Bits" below.
|
|
231
|
+
- **Sequential first, parallel later**: Start with sequential execution within stages. Add `joblib.Parallel` or `concurrent.futures` as an optimization later.
|
|
232
|
+
|
|
233
|
+
### Phase 4: LLM Integration
|
|
234
|
+
|
|
235
|
+
**Files**: `feature_engineer.py` system prompts
|
|
236
|
+
|
|
237
|
+
Update the `generate()` and `generate_next()` prompts to tell the LLM about `depends_on`:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
Each step_spec can optionally include "depends_on": ["step_id_1", "step_id_2"]
|
|
241
|
+
to declare that this step requires columns created by other steps.
|
|
242
|
+
If a step only uses original dataset columns, omit depends_on or set it to [].
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The LLM doesn't need to worry about ordering — just declare dependencies. The compiler handles the rest.
|
|
246
|
+
|
|
247
|
+
### Phase 5: Autotrain Agent Integration
|
|
248
|
+
|
|
249
|
+
**Files**: `feature_engineering.py` handler, `batch_apply_steps_tool`
|
|
250
|
+
|
|
251
|
+
- `batch_apply_steps_tool` already accepts a list of step specs and creates a single `PipelineSpec`. No changes needed — the DAG resolution happens at compile time.
|
|
252
|
+
- `analyze_feature_dependencies_tool` can be simplified to just set `depends_on` on each step based on column analysis, rather than trying to reorder indices.
|
|
253
|
+
- Interactive mode: as the user approves features one at a time, the `depends_on` edges naturally build up. A new feature can reference previously applied feature IDs.
|
|
254
|
+
|
|
255
|
+
## Tricky Bits
|
|
256
|
+
|
|
257
|
+
### 1. Schema Propagation
|
|
258
|
+
|
|
259
|
+
The hardest problem. Within a stage, transformer A might create column `foo` that transformer B in the **next** stage needs. The compiler needs to know the intermediate schema at each stage to validate column references.
|
|
260
|
+
|
|
261
|
+
**Options**:
|
|
262
|
+
- **Lazy (recommended for v1)**: Don't validate column existence at compile time. Let it fail at runtime with a clear error: "Step 'X' references column 'foo' which doesn't exist. Check depends_on."
|
|
263
|
+
- **Eager (future)**: Add an `output_schema` field to `StepSpec` that declares what columns a step adds/removes/modifies. The compiler walks the DAG and checks that every step's input columns exist in the union of original columns + upstream output columns.
|
|
264
|
+
|
|
265
|
+
### 2. Column Conflicts Within a Stage
|
|
266
|
+
|
|
267
|
+
If two steps in the same stage both modify column `age`, the result is ambiguous.
|
|
268
|
+
|
|
269
|
+
**Solution**: Validate at compile time that steps within a stage don't write to the same columns. Steps that create new columns (ExpressionTransformer) are fine. Steps that modify existing columns (StandardScaler on `age`) must not share target columns with other steps in the same stage.
|
|
270
|
+
|
|
271
|
+
### 3. Drop Steps
|
|
272
|
+
|
|
273
|
+
`DropColumnsTransformer` is tricky in a DAG because you need to ensure no downstream step needs the dropped column.
|
|
274
|
+
|
|
275
|
+
**Solution**: Drop steps should depend on all steps that read the column being dropped. The compiler can warn if a drop step doesn't depend on all readers.
|
|
276
|
+
|
|
277
|
+
### 4. Fitted State Serialization
|
|
278
|
+
|
|
279
|
+
Currently `save_pipeline` / `load_pipeline` use cloudpickle on the entire `DataFramePipeline`. A `DAGPipeline` has a `stages` structure instead of flat `steps`. Cloudpickle should handle this transparently since it serializes the object graph. But test this explicitly.
|
|
280
|
+
|
|
281
|
+
### 5. Backwards Compatibility
|
|
282
|
+
|
|
283
|
+
A `PipelineSpec` with no `depends_on` on any step is exactly today's flat pipeline. The compiler detects this and returns a `DataFramePipeline`. Zero breaking changes for existing specs in the database.
|
|
284
|
+
|
|
285
|
+
## Testing Strategy
|
|
286
|
+
|
|
287
|
+
1. **Unit: `topological_stages()`** — Valid DAG, diamond dependency, linear chain, single node, disjoint subgraphs, cycle detection
|
|
288
|
+
2. **Unit: `DAGPipeline.fit_transform()`** — Two independent branches merge into one step, verify output columns are correct
|
|
289
|
+
3. **Integration: `compile_spec()` with DAG** — Full round-trip: spec with `depends_on` -> compile -> fit -> transform -> verify output
|
|
290
|
+
4. **Regression: Flat specs** — All existing 88 tests must still pass unchanged
|
|
291
|
+
5. **Edge cases**: Step depends on itself, step depends on non-existent ID, two steps create same output column in same stage
|
|
292
|
+
|
|
293
|
+
## Recommended Implementation Order
|
|
294
|
+
|
|
295
|
+
| Step | Effort | Risk | What it unblocks |
|
|
296
|
+
|------|--------|------|-----------------|
|
|
297
|
+
| 1. `depends_on` field + validation | Small | None | Everything else |
|
|
298
|
+
| 2. `topological_stages()` | Small | None | DAGPipeline |
|
|
299
|
+
| 3. `DAGPipeline` (sequential within stages) | Medium | Medium | Full DAG execution |
|
|
300
|
+
| 4. `compile_spec()` DAG branch | Small | Low | End-to-end usage |
|
|
301
|
+
| 5. LLM prompt updates | Small | Low | Agent generates DAGs |
|
|
302
|
+
| 6. Parallel execution within stages | Medium | Medium | Performance gain |
|
|
303
|
+
| 7. Schema propagation validation | Large | High | Compile-time safety |
|
|
304
|
+
|
|
305
|
+
Steps 1-4 give you a working DAG pipeline. Step 5 lets the LLM use it. Steps 6-7 are optimizations you can add later.
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# Feature Pipeline Architectures: Alternatives Analysis
|
|
2
|
+
|
|
3
|
+
## The Core Problem
|
|
4
|
+
|
|
5
|
+
How should an LLM express feature transformations so they're executable, composable, and don't require manual ordering?
|
|
6
|
+
|
|
7
|
+
The current system (after the inline step_spec work) has the LLM produce flat, ordered step specs. This works but pushes ordering logic onto the LLM and limits composability. There are several fundamentally different paradigms worth considering.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. DAG Pipeline
|
|
12
|
+
|
|
13
|
+
**See**: `dag-pipeline-proposal.md` for full implementation plan.
|
|
14
|
+
|
|
15
|
+
Steps declare dependencies via `depends_on`. The compiler topologically sorts and groups into parallel execution stages.
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"steps": [
|
|
20
|
+
{"id": "extract_dow", "type": "DateTimeExtractTransformer", "depends_on": []},
|
|
21
|
+
{"id": "rolling_sales", "type": "RollingAggTransformer", "depends_on": []},
|
|
22
|
+
{"id": "interaction", "type": "ExpressionTransformer", "depends_on": ["extract_dow", "rolling_sales"]}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Strengths**:
|
|
28
|
+
- Closest to current model — additive change to StepSpec
|
|
29
|
+
- Explicit dependencies are debuggable and inspectable
|
|
30
|
+
- Enables parallel execution within stages
|
|
31
|
+
- LLM no longer needs to worry about ordering
|
|
32
|
+
|
|
33
|
+
**Weaknesses**:
|
|
34
|
+
- LLM still thinks in terms of transformer types and wiring
|
|
35
|
+
- Schema propagation between stages is complex to validate at compile time
|
|
36
|
+
- Adding a step mid-graph requires understanding the full dependency structure
|
|
37
|
+
|
|
38
|
+
**Effort**: Medium. Builds directly on existing schema/compiler/pipeline.
|
|
39
|
+
|
|
40
|
+
**Best for**: Incremental improvement with low risk.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Declarative Column Specs
|
|
45
|
+
|
|
46
|
+
Instead of "run these transformers in this order", the LLM declares "I want these output columns defined by these expressions." The compiler resolves what transformers to use and in what order.
|
|
47
|
+
|
|
48
|
+
### Spec Format
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"version": "3.0",
|
|
53
|
+
"target_columns": {
|
|
54
|
+
"age_income_ratio": {
|
|
55
|
+
"expr": "age / income",
|
|
56
|
+
"description": "Ratio of age to income"
|
|
57
|
+
},
|
|
58
|
+
"log_salary": {
|
|
59
|
+
"expr": "log(salary)",
|
|
60
|
+
"description": "Log-transformed salary to reduce skewness"
|
|
61
|
+
},
|
|
62
|
+
"order_dayofweek": {
|
|
63
|
+
"extract": "dayofweek",
|
|
64
|
+
"from": "order_date",
|
|
65
|
+
"description": "Day of week from order timestamp"
|
|
66
|
+
},
|
|
67
|
+
"sales_7d_avg": {
|
|
68
|
+
"rolling": "mean",
|
|
69
|
+
"column": "sales",
|
|
70
|
+
"window": 7,
|
|
71
|
+
"group_by": ["store_id"],
|
|
72
|
+
"order_by": "order_date",
|
|
73
|
+
"description": "7-day rolling average sales per store"
|
|
74
|
+
},
|
|
75
|
+
"risk_category": {
|
|
76
|
+
"bin": "risk_score",
|
|
77
|
+
"strategy": "quantile",
|
|
78
|
+
"n_bins": 5,
|
|
79
|
+
"description": "Risk score bucketed into quintiles"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"drop_columns": ["order_date", "customer_name"],
|
|
83
|
+
"transformations": {
|
|
84
|
+
"salary": {"impute": "median"},
|
|
85
|
+
"country": {"encode": "onehot"}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### How the Compiler Works
|
|
91
|
+
|
|
92
|
+
1. Parse `target_columns` — each is a column definition with a known pattern (`expr`, `extract`, `rolling`, `bin`, etc.)
|
|
93
|
+
2. Map each pattern to a transformer type:
|
|
94
|
+
- `expr` → `ExpressionTransformer`
|
|
95
|
+
- `extract` → `DateTimeExtractTransformer`
|
|
96
|
+
- `rolling` → `RollingAggTransformer`
|
|
97
|
+
- `bin` → `KBinsDiscretizer`
|
|
98
|
+
3. Infer dependencies by parsing column references in expressions
|
|
99
|
+
4. Topologically sort and compile to a `DataFramePipeline` (or `DAGPipeline`)
|
|
100
|
+
|
|
101
|
+
### What the LLM Prompt Looks Like
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
You are a data scientist. Given a dataset summary, declare what new columns
|
|
105
|
+
would improve model performance.
|
|
106
|
+
|
|
107
|
+
Available column definition patterns:
|
|
108
|
+
- expr: pandas expression (e.g. "col_a * col_b", "log(price)")
|
|
109
|
+
- extract: datetime component (dayofweek, month, hour, year)
|
|
110
|
+
- rolling: rolling window aggregate (mean, sum, std, min, max)
|
|
111
|
+
- lag: grouped lag features
|
|
112
|
+
- bin: discretize continuous values
|
|
113
|
+
- custom: Python code for anything else
|
|
114
|
+
|
|
115
|
+
Respond with a JSON object listing desired columns:
|
|
116
|
+
{"target_columns": {"column_name": {"expr": "...", "description": "..."}}}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Strengths**:
|
|
120
|
+
- Better abstraction for LLMs — "what do you want?" not "how do you build it?"
|
|
121
|
+
- Dependencies are implicit (parsed from expressions) — no `depends_on` to manage
|
|
122
|
+
- Closer to how data scientists think: "I want a column called X"
|
|
123
|
+
- The DSL is small and learnable — ~6 patterns cover 95% of feature engineering
|
|
124
|
+
- Compiler owns all transformer selection logic — single source of truth
|
|
125
|
+
- Naturally deduplicates: two recommendations that want the same column just merge
|
|
126
|
+
|
|
127
|
+
**Weaknesses**:
|
|
128
|
+
- Requires designing a column definition DSL (the patterns above)
|
|
129
|
+
- Expression parsing needs to extract column references reliably
|
|
130
|
+
- Complex features (custom sklearn transformers) still need an escape hatch
|
|
131
|
+
- More upfront design than DAG, which is purely additive
|
|
132
|
+
|
|
133
|
+
**Effort**: Medium-large. New spec format, new compiler path, new prompt design.
|
|
134
|
+
|
|
135
|
+
**Best for**: Clean long-term architecture. Ideal if rebuilding the spec layer.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 3. Expression Graph (Polars-Style Lazy Evaluation)
|
|
140
|
+
|
|
141
|
+
Every feature is a lazy column expression. The engine builds a computation graph, optimizes it (predicate pushdown, common subexpression elimination, projection pushdown), then executes.
|
|
142
|
+
|
|
143
|
+
### What It Looks Like
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import polars as pl
|
|
147
|
+
|
|
148
|
+
features = (
|
|
149
|
+
df.lazy()
|
|
150
|
+
.with_columns([
|
|
151
|
+
(pl.col("age") / pl.col("income")).alias("age_income_ratio"),
|
|
152
|
+
pl.col("salary").log().alias("log_salary"),
|
|
153
|
+
pl.col("order_date").dt.weekday().alias("order_dow"),
|
|
154
|
+
pl.col("sales").rolling_mean(7).over("store_id").alias("sales_7d_avg"),
|
|
155
|
+
])
|
|
156
|
+
.drop(["order_date", "customer_name"])
|
|
157
|
+
.collect()
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Architecture
|
|
162
|
+
|
|
163
|
+
- LLM generates Polars expressions (or a JSON DSL that compiles to Polars)
|
|
164
|
+
- The lazy evaluation engine handles ordering, optimization, and execution
|
|
165
|
+
- No manual pipeline construction needed — the engine IS the pipeline
|
|
166
|
+
|
|
167
|
+
### Spec Format (JSON → Polars)
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"expressions": [
|
|
172
|
+
{"alias": "age_income_ratio", "expr": "col('age') / col('income')"},
|
|
173
|
+
{"alias": "log_salary", "expr": "col('salary').log()"},
|
|
174
|
+
{"alias": "order_dow", "expr": "col('order_date').dt.weekday()"}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A thin compiler converts these to `pl.Expr` objects.
|
|
180
|
+
|
|
181
|
+
**Strengths**:
|
|
182
|
+
- 10-100x faster than sequential pandas for large datasets
|
|
183
|
+
- Automatic query optimization — the engine handles parallelism and ordering
|
|
184
|
+
- No pipeline/DAG/ordering logic to maintain — the engine does it all
|
|
185
|
+
- Polars expressions are more powerful and composable than sklearn transformers
|
|
186
|
+
- Memory efficient: streaming execution, no intermediate DataFrame copies
|
|
187
|
+
|
|
188
|
+
**Weaknesses**:
|
|
189
|
+
- Requires Polars dependency (currently using pandas + sklearn)
|
|
190
|
+
- sklearn transformers (StandardScaler, OneHotEncoder, etc.) don't work with Polars natively — need wrappers or reimplementation
|
|
191
|
+
- Fit/transform semantics don't exist in Polars — need to handle stateful transforms (e.g., imputer learns mean from training data, applies to new data) separately
|
|
192
|
+
- LLMs are more familiar with pandas than Polars syntax
|
|
193
|
+
- Large migration from current architecture
|
|
194
|
+
|
|
195
|
+
**Effort**: Large. New runtime, new serialization, adapter layer for sklearn.
|
|
196
|
+
|
|
197
|
+
**Best for**: Performance-critical pipelines with large datasets. Consider if pandas becomes the bottleneck.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 4. Feature Store Pattern
|
|
202
|
+
|
|
203
|
+
Each feature is a standalone, versioned computation with a defined contract: input columns, output columns, and the transformation logic. Features are registered in a catalog and can be composed across models.
|
|
204
|
+
|
|
205
|
+
### Architecture
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
FeatureRegistry
|
|
209
|
+
├── age_income_ratio (v1)
|
|
210
|
+
│ ├── inputs: [age, income]
|
|
211
|
+
│ ├── outputs: [age_income_ratio]
|
|
212
|
+
│ ├── transformer: ExpressionTransformer(...)
|
|
213
|
+
│ └── fitted_state: {...}
|
|
214
|
+
├── sales_7d_avg (v2)
|
|
215
|
+
│ ├── inputs: [sales, store_id, order_date]
|
|
216
|
+
│ ├── outputs: [sales_mean_7]
|
|
217
|
+
│ ├── transformer: RollingAggTransformer(...)
|
|
218
|
+
│ └── fitted_state: {...}
|
|
219
|
+
└── ...
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### How It Works
|
|
223
|
+
|
|
224
|
+
1. LLM recommends features → each becomes a registered feature with input/output contract
|
|
225
|
+
2. User selects features → system resolves dependencies from input/output declarations
|
|
226
|
+
3. Compiler builds a pipeline from selected features (ordering resolved from contracts)
|
|
227
|
+
4. Features are cached — once computed for a dataset, they're reused across models
|
|
228
|
+
5. Features are versioned — changing the definition creates a new version
|
|
229
|
+
|
|
230
|
+
### Spec Format
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"features": ["age_income_ratio@v1", "sales_7d_avg@v2", "order_dow@v1"],
|
|
235
|
+
"drop": ["order_date"],
|
|
236
|
+
"target": "churn"
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The spec is just a list of feature references. The registry has all the logic.
|
|
241
|
+
|
|
242
|
+
**Strengths**:
|
|
243
|
+
- Features become reusable assets across autotrain runs
|
|
244
|
+
- Natural caching: compute once, use everywhere
|
|
245
|
+
- Versioning: change a feature definition without breaking existing models
|
|
246
|
+
- Input/output contracts make dependency resolution trivial
|
|
247
|
+
- Testing is per-feature, not per-pipeline
|
|
248
|
+
- Aligns with industry direction (Feast, Tecton, Featureform)
|
|
249
|
+
|
|
250
|
+
**Weaknesses**:
|
|
251
|
+
- Heavyweight for single-run autotrain — overhead only pays off with feature reuse
|
|
252
|
+
- Requires a feature registry (new infrastructure)
|
|
253
|
+
- Fitted state becomes per-feature — serialization/loading is more complex
|
|
254
|
+
- LLM needs to know what features already exist in the registry (growing context)
|
|
255
|
+
|
|
256
|
+
**Effort**: Large. New registry, versioning system, caching layer.
|
|
257
|
+
|
|
258
|
+
**Best for**: Platform with many models sharing features. Overkill for single autotrain runs today, but could become the right abstraction as the platform scales.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 5. Auto-Feature Synthesis (Featuretools-Style)
|
|
263
|
+
|
|
264
|
+
Algorithmic feature generation, not LLM-driven. Given columns and their types, systematically generate all meaningful combinations and score them against the target.
|
|
265
|
+
|
|
266
|
+
### How It Works
|
|
267
|
+
|
|
268
|
+
1. Define "primitives": ratio, product, sum, difference, log, sqrt, datetime_extract, grouped_agg
|
|
269
|
+
2. Enumerate: for each pair of numeric columns, generate ratio, product, difference. For each datetime, extract all components. For each numeric + categorical pair, generate grouped aggregates.
|
|
270
|
+
3. Score: compute mutual information (or correlation, or permutation importance) between each candidate feature and the target variable
|
|
271
|
+
4. Rank and select: keep the top N features
|
|
272
|
+
|
|
273
|
+
### Integration with Current System
|
|
274
|
+
|
|
275
|
+
This doesn't replace LLM recommendations — it complements them:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
Auto-mode pipeline:
|
|
279
|
+
1. Auto-feature synthesis → 50 candidate features, scored
|
|
280
|
+
2. Filter to top 10 by mutual information
|
|
281
|
+
3. LLM reviews candidates → refines, combines, adds domain insight
|
|
282
|
+
4. Final feature set → compile and apply
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Or simpler:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
Auto-mode pipeline:
|
|
289
|
+
1. LLM generates 5 creative features (current flow)
|
|
290
|
+
2. Auto-synthesis generates 20 mechanical features (ratios, interactions)
|
|
291
|
+
3. Score all 25 against target
|
|
292
|
+
4. Keep top N
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Strengths**:
|
|
296
|
+
- Exhaustive: finds features the LLM wouldn't think of (e.g., `col7 * col13 / col2` that happens to be predictive)
|
|
297
|
+
- No hallucination risk — purely data-driven
|
|
298
|
+
- Deterministic and reproducible
|
|
299
|
+
- Can run without any LLM calls
|
|
300
|
+
- Well-established technique (Featuretools, AutoFeat, tsfresh)
|
|
301
|
+
|
|
302
|
+
**Weaknesses**:
|
|
303
|
+
- Combinatorial explosion: N columns → O(N^2) pairwise features, O(N^3) for triples
|
|
304
|
+
- Generates many useless features — needs aggressive filtering
|
|
305
|
+
- Requires a target variable (supervised) for scoring
|
|
306
|
+
- Features lack semantic meaning — "col3_times_col7" isn't interpretable
|
|
307
|
+
- Doesn't handle complex domain-specific features (e.g., "customer lifetime value")
|
|
308
|
+
|
|
309
|
+
**Effort**: Medium. Can be implemented as a standalone tool alongside LLM recommendations.
|
|
310
|
+
|
|
311
|
+
**Best for**: Auto-mode where speed matters more than interpretability. Excellent complement to LLM-generated features.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Comparison Matrix
|
|
316
|
+
|
|
317
|
+
| Approach | Ordering Problem | LLM Cognitive Load | Performance | Reusability | Migration Effort |
|
|
318
|
+
|----------|-----------------|-------------------|-------------|-------------|-----------------|
|
|
319
|
+
| **Current (flat)** | LLM must order | High | Sequential | None | N/A |
|
|
320
|
+
| **DAG** | Compiler resolves | Medium | Parallelizable | None | Small |
|
|
321
|
+
| **Declarative columns** | Implicit from expressions | Low | Parallelizable | None | Medium |
|
|
322
|
+
| **Expression graph** | Engine resolves | Low | Optimal | None | Large |
|
|
323
|
+
| **Feature store** | Contracts resolve | Low | Cached | Full reuse | Large |
|
|
324
|
+
| **Auto-synthesis** | N/A (scored) | None | Parallel | Cacheable | Medium |
|
|
325
|
+
|
|
326
|
+
## Recommended Path
|
|
327
|
+
|
|
328
|
+
### Short-term (now → 1 month)
|
|
329
|
+
**DAG** — additive to existing spec, solves the immediate ordering problem, low risk.
|
|
330
|
+
|
|
331
|
+
### Medium-term (1-3 months)
|
|
332
|
+
**Declarative column specs** — design the DSL, build a compiler that maps column definitions to the existing transformer infrastructure. This becomes the LLM-facing abstraction while the DAG handles execution underneath.
|
|
333
|
+
|
|
334
|
+
### Long-term (3-6 months)
|
|
335
|
+
**Feature store** — as the platform handles more models, feature reuse becomes valuable. The declarative column spec format naturally evolves into feature definitions with input/output contracts.
|
|
336
|
+
|
|
337
|
+
### Orthogonal (can start anytime)
|
|
338
|
+
**Auto-feature synthesis** — independent of pipeline architecture. Add as an auto-mode enhancement alongside LLM recommendations. Start with pairwise numeric interactions + datetime extractions, score by mutual information, filter aggressively.
|
|
339
|
+
|
|
340
|
+
### Consider later
|
|
341
|
+
**Expression graph (Polars)** — only pursue if pandas performance becomes a bottleneck on real workloads. The migration cost is high and the current DataFrame sizes in autotrain may not justify it.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Architecture Layering
|
|
346
|
+
|
|
347
|
+
These approaches aren't mutually exclusive. The ideal architecture layers them:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
┌─────────────────────────────────────────┐
|
|
351
|
+
│ LLM / Auto-Synthesis (feature ideas) │ ← what columns to create
|
|
352
|
+
├─────────────────────────────────────────┤
|
|
353
|
+
│ Declarative Column Specs (DSL) │ ← user-facing format
|
|
354
|
+
├─────────────────────────────────────────┤
|
|
355
|
+
│ DAG Compiler (dependency resolution) │ ← execution planning
|
|
356
|
+
├─────────────────────────────────────────┤
|
|
357
|
+
│ Pipeline Runtime (execution) │ ← fit/transform
|
|
358
|
+
├─────────────────────────────────────────┤
|
|
359
|
+
│ Feature Store (versioning + caching) │ ← persistence + reuse
|
|
360
|
+
└─────────────────────────────────────────┘
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Each layer can be built independently. The inline step_spec work we just completed is the foundation — it gives us structured specs flowing from the LLM. Every architecture above builds on that.
|