Spaces:
Sleeping
Sleeping
| # P2 Feature & Target Engineering Tools | |
| ** Aditya Β· Sprint: June 1β12 Β· Status: shipped (registry 18 β 22 tools in this snapshot)** | |
| Four tools that close the gap between "I have a SQL result" and "I can | |
| train an honest model on it": `derive_feature`, `define_target`, | |
| `time_split`, `handle_imbalance`. Specs follow `docs/v1_tools.md` Β§5; | |
| this doc is the implementation reference. | |
| The canonical predictive chain is now: | |
| ``` | |
| run_sql(label="loans") | |
| β derive_feature(df_label="loans", name="payment_burden", | |
| expression="payments / amount") | |
| β define_target(df_label="loans", target_column="y_default", | |
| positive_definition="status = 'B'", | |
| scope_filter="status IN ('A','B')", | |
| entity_column="loan_id") | |
| β time_split(df_label="loans", time_col="granted_date", | |
| cutoff="2017-06-01") | |
| β handle_imbalance(df_label="train") # defaults target from define_target | |
| β train_tabular_model(df_label="train", target_column="y_default", ...) | |
| β predict / evaluate_predictions on "test" | |
| ``` | |
| --- | |
| ## 1. `derive_feature` | |
| `lexsi_ds/agent/tools/derive_feature.py` | |
| Adds ONE column to a cached `run_sql` DataFrame from a scalar | |
| expression β a ratio, date diff, or CASE bucket β without a SQL | |
| round-trip. Mutates `sql_result:<df_label>` in place (v1_tools Β§5.1 | |
| contract). | |
| | Arg | Type | Default | Notes | | |
| |---|---|---|---| | |
| | `df_label` | `str` | β | cached frame to mutate | | |
| | `name` | `str` | β | new column; identifier-validated | | |
| | `expression` | `str` | β | scalar SQL (duckdb) or `df.eval` (pandas) | | |
| | `engine` | `"duckdb" \| "pandas"` | `"duckdb"` | duckdb is binder-validated | | |
| | `overwrite` | `bool` | `False` | guard against clobbering source columns | | |
| Failure modes surfaced as `ok=False` with a recoverable observation: | |
| `unknown_label` (lists cached labels), binder errors (lists available | |
| columns), `column_exists`, `forbidden_expression` (statement-level SQL | |
| keywords blocked), `non_rowwise_expression` (aggregates rejected β | |
| those belong in `run_sql`). | |
| ## 2. `define_target` | |
| `lexsi_ds/agent/tools/define_target.py` | |
| Materializes the prediction target as an explicit column and records | |
| the **recipe** β task type, positive definition, scope filter, entity | |
| column β under `last_target_definition`. Fixes the v0 failure mode of | |
| training on a raw multi-code column when the question was binary. | |
| | Arg | Type | Default | Notes | | |
| |---|---|---|---| | |
| | `df_label` | `str` | β | | | |
| | `target_column` | `str` | β | created if `positive_definition` given, else validated | | |
| | `task_type` | `"classification" \| "regression"` | `"classification"` | | | |
| | `positive_definition` | `str \| None` | `None` | SQL boolean (cls) / numeric expr (reg) | | |
| | `scope_filter` | `str \| None` | `None` | rows kept for training; rest dropped | | |
| | `entity_column` | `str \| None` | `None` | recorded so training excludes it | | |
| Validation: β₯2 classes for classification (else `single_class_target`), | |
| β€20 classes (else `high_cardinality_target` β likely a regression | |
| target), numeric dtype for regression, NULL-target rows flagged. | |
| ## 3. `time_split` | |
| `lexsi_ds/agent/tools/time_split.py` | |
| Temporal train/test split: train = strictly before the cutoff, test = | |
| at/after. Two modes β explicit `cutoff` (ISO date) or `test_fraction` | |
| (chronologically-last fraction; the derived cutoff is reported). | |
| VARCHAR date columns are coerced; >20% parse failures β | |
| `bad_time_column`. A cutoff outside the data range β `degenerate_split` | |
| with the actual range in the observation so the planner can recover. | |
| The observation states the no-overlap invariant explicitly ("every | |
| train row precedes every test row") so the summariser can cite it. | |
| Cache: `sql_result:<train_label>`, `sql_result:<test_label>`, | |
| `last_split = {kind:"time", time_col, cutoff, train_label, test_label, | |
| n_train, n_test, source_label}`. Sides under 50 rows are warned per the | |
| spec. | |
| ## 4. `handle_imbalance` | |
| `lexsi_ds/agent/tools/handle_imbalance.py` | |
| Class-balance report + **ranked strategy proposal β it never resamples | |
| itself** (the planner/user decides; silent data surgery is not | |
| shippable in an enterprise deployment). Defaults its target from | |
| `last_target_definition` so it chains naturally after `define_target`. | |
| Severity bands on the imbalance ratio (majority/minority): balanced | |
| (<1.5), mild (<3), moderate (<10), severe (β₯10). Strategy ranking is | |
| data-aware: class weights always first; undersampling only when the | |
| majority can spare rows (β₯2k); SMOTE/oversampling when the minority is | |
| tiny (<200); **Lexsi synthetic augmentation offered only when | |
| `ctx.org` is attached** (`train_synthetic_model` β | |
| `generate_synthetic_data_points` on the platform), so offline runs stay | |
| clean; metric guidance (AUROC/PR-AUC over accuracy) always appended for | |
| moderate/severe. Continuous targets (>20 distinct values) are rejected | |
| with `non_categorical_target` and a pointer at the regression path. | |
| Cache: `last_imbalance_report`. | |
| --- | |
| ## 5. Feature lineage (enterprise upgrade, this iteration) | |
| `lexsi_ds/agent/tools/_lineage.py` | |
| All four tools append a structured record to | |
| `ctx.cache["feature_lineage"]` (ordinal, tool, UTC timestamp, | |
| operation details). This gives every trained model a reproducible | |
| "how the training frame was built" recipe β the model-risk-management | |
| audit artifact regulated customers ask for, and the data source for the | |
| planned `export_report` (P3) audit section. Append-only, run-scoped, | |
| zero hot-path cost. | |
| ## 6. New planner rules | |
| Rules **20β23** added to `PLANNER_SYSTEM` (`lexsi_ds/agent/prompts.py`), | |
| one per tool, additive over 1β19: | |
| - **20** derive features with `derive_feature`, don't re-run SQL | |
| - **21** time-split temporal data before training β never random-split it | |
| - **22** `define_target` whenever the label must be constructed | |
| - **23** `handle_imbalance` before classification training; cite AUROC/PR-AUC when moderate/severe | |
| > Merge note: Bhavish's June-10 branch also claims rules 20β22 for his | |
| > P1 tools (`detect_data_quality_issues`, `suggest_join_paths`, | |
| > `sample_rows`). Merges shoud include second renumbers β the rules are | |
| > independent prose blocks, so it's a mechanical shift. | |
| ## 7. Benchmark coverage | |
| `bench/pkdd/p2_feature_engineering.yaml` β 10 items: | |
| - happy + error path per tool (8), trajectory-scored: expected/forbidden | |
| tools, `ok` flag, error code, observation substrings, cache keys | |
| written; | |
| - the **leakage pair** (2): the same default-risk question random-split | |
| vs time-split. Passes when the time-split run trains strictly | |
| pre-cutoff and reports an AUROC **lower** than the leaky random-split | |
| AUROC β demonstrating the leak rather than asserting a fixed number. | |
| This validates demo step 3. | |
| ## 8. Tests | |
| `tests/test_p2_feature_engineering.py` β 25 hermetic tests (in-memory | |
| DuckDB, no PKDD file, no SDK, no LLM), reusing the offline-synthetic | |
| fixture family from `conftest.py` plus a local `temporal_loan_df` | |
| fixture with a time-drifting label. Covers every happy/error path | |
| above, the cache contracts, the no-temporal-overlap invariant, lineage | |
| ordering, and registry wiring. | |