lexsi-ds-agent / docs /p2_feature_engineering.md
bp-lexsi's picture
Add P1/P2 agent tools: data-quality, sampling, join-paths, feature/target engineering
8bf2593
|
Raw
History Blame Contribute Delete
7.16 kB
# 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.