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.
Files changed (33) hide show
  1. xplainable_preprocessing-0.1.0/PKG-INFO +13 -0
  2. xplainable_preprocessing-0.1.0/docs/dag-pipeline-proposal.md +305 -0
  3. xplainable_preprocessing-0.1.0/docs/feature-pipeline-architectures.md +363 -0
  4. xplainable_preprocessing-0.1.0/docs/feature-store-proposal.md +530 -0
  5. xplainable_preprocessing-0.1.0/pyproject.toml +28 -0
  6. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/__init__.py +20 -0
  7. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/compiler.py +68 -0
  8. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/pipeline.py +87 -0
  9. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/preview.py +137 -0
  10. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/registry.py +108 -0
  11. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/sandbox.py +113 -0
  12. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/schema.py +71 -0
  13. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/serialization.py +20 -0
  14. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/__init__.py +23 -0
  15. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/category_condense.py +50 -0
  16. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/datetime_extract.py +72 -0
  17. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/drop_columns.py +25 -0
  18. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/expression.py +35 -0
  19. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/fill_missing.py +71 -0
  20. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/grouped_lag.py +55 -0
  21. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/rename_columns.py +25 -0
  22. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/rolling_agg.py +79 -0
  23. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/text_clean.py +70 -0
  24. xplainable_preprocessing-0.1.0/src/xplainable_preprocessing/transformers/type_cast.py +42 -0
  25. xplainable_preprocessing-0.1.0/tests/__init__.py +0 -0
  26. xplainable_preprocessing-0.1.0/tests/test_compiler.py +120 -0
  27. xplainable_preprocessing-0.1.0/tests/test_preview.py +90 -0
  28. xplainable_preprocessing-0.1.0/tests/test_sandbox.py +132 -0
  29. xplainable_preprocessing-0.1.0/tests/test_schema.py +109 -0
  30. xplainable_preprocessing-0.1.0/tests/test_serialization.py +83 -0
  31. xplainable_preprocessing-0.1.0/tests/test_transformers/__init__.py +0 -0
  32. xplainable_preprocessing-0.1.0/tests/test_transformers/test_all_transformers.py +228 -0
  33. 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.